java多线程(超详细)

article/2025/10/9 13:52:18

1 - 线程

1.1 - 进程

进程就是正在运行中的程序(进程是驻留在内存中的)

  • 是系统执行资源分配和调度的独立单位

  • 每一进程都有属于自己的存储空间和系统资源

  • 注意:进程A和进程B的内存独立不共享。

1.2 - 线程

线程就是进程中的单个顺序控制流,也可以理解成是一条执行路径

  • 单线程:一个进程中包含一个顺序控制流(一条执行路径)

  • 多线程:一个进程中包含多个顺序控制流(多条执行路径)

  • 在java语言中:
     线程A和线程B,堆内存和方法区内存共享。
     但是栈内存独立,一个线程一个栈。

  • 假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。

  • java中之所以有多线程机制,目的就是为了提高程序的处理效率。

  • 对于单核的CPU来说,不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是多个事情同时在做。

1.3 -java中多线程的生命周期

 就绪状态:就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺CPU时间片的权力(CPU时间片就是执行权)。当一个线程抢夺到CPU时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态。

运行状态:run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片,当再次抢到CPU时间之后,会重新进入run方法接着上一次的代码继续往下执行。

阻塞状态:当一个线程遇到阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片。之前的时间片没了需要再次回到就绪状态抢夺CPU时间片。

锁池:在这里找共享对象的对象锁线程进入锁池找共享对象的对象锁的时候,会释放之前占有CPU时间片,有可能找到了,有可能没找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺CPU时间片。(这个进入锁池,可以理解为一种阻塞状态)

1.4 - 多线程的实现方式(一)

  • 继承Thread类

    1、自定义一个类MyThread类,用来继承与Thread类

    2、在MyThread类中重写run()方法

    3、在测试类中创建MyThread类的对象

    4、启动线程

/*** @author Mr.乐* @Description*/
public class Demo01 {public static void main(String[] args) {//创建线程MyThread t01 = new MyThread();MyThread t02 = new MyThread();MyThread t03 = new MyThread("线程03");//开启线程
//        t01.run();
//        t02.run();
//        t03.run();// 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)// start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。// 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。// 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。// run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。t01.start();t02.start();t03.start();//设置线程名(补救的设置线程名的方式)t01.setName("线程01");t02.setName("线程02");//设置主线程名称Thread.currentThread().setName("主线程");for (int i = 0; i < 50; i++) {//Thread.currentThread() 获取当前正在执行线程的对象System.out.println(Thread.currentThread().getName() + ":" + i);}}
}
class MyThread extends Thread{public MyThread() {}public MyThread(String name) {super(name);}//run方法是每个线程运行过程中都必须执行的方法@Overridepublic void run() {for (int i = 0; i < 50; i++) {System.out.println(this.getName() + ":" + i);}}
}

此处最重要的为start()方法。单纯调用run()方法不会启动线程,不会分配新的分支栈。

start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。线程就启动成功了。

启动成功的线程会自动调用run方法(由JVM线程调度机制来运作的),并且run方法在分支栈的栈底部(压栈)。

run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。

单纯使用run()方法是不能多线程并发的。

1.5 - 设置和获取线程名

  • 设置线程名

    • setName(String name):设置线程名

    • 通过带参构造方法设置线程名

  • 获取线程名

    • getName():返回字符串形式的线程名

    • Thread.CurrentThread():返回当前正在执行的线程对象

1.6 - 多线程的实现方式(二)

  • 实现Runnable接口

    1、自定义一个MyRunnable类来实现Runnable接口

    2、在MyRunnable类中重写run()方法

    3、创建Thread对象,并把MyRunnable对象作为Tread类构造方法的参数传递进去

    4、启动线程

/*** @author Mr.乐* @Description*/
public class Demo02 {public static void main(String[] args) {MyRunnable myRun = new MyRunnable();//将一个任务提取出来,让多个线程共同去执行//封装线程对象Thread t01 = new Thread(myRun, "线程01");Thread t02 = new Thread(myRun, "线程02");Thread t03 = new Thread(myRun, "线程03");//开启线程t01.start();t02.start();t03.start();//通过匿名内部类的方式创建线程new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + " - " + i);}}},"线程04").start();}
}
//自定义线程类,实现Runnable接口
//这并不是一个线程类,是一个可运行的类,它还不是一个线程。
class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 50; i++) {System.out.println(Thread.currentThread().getName() + " - " + i);}}
}

1.7 - 多线程的实现方式(三)

实现Callable接口( java.util.concurrent.FutureTask; /JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。)

1、自定义一个MyCallable类来实现Callable接口

2、在MyCallable类中重写call()方法

3、创建FutureTask,Thread对象,并把MyCallable对象作为FutureTask类构造方法的参数传递进去,把FutureTask对象传递给Thread对象。

4、启动线程

        这种方式的优点:可以获取到线程的执行结果。

        这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/*** @author Mr.乐* @Description  线程实现的第三种方式*/
public class Demo04 {public static void main(String[] args) throws Exception {// 第一步:创建一个“未来任务类”对象。// 参数非常重要,需要给一个Callable接口实现类对象。FutureTask task = new FutureTask(new Callable() {@Overridepublic Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值// 线程执行一个任务,执行之后可能会有一个执行结果// 模拟执行System.out.println("call method begin");Thread.sleep(1000 * 10);System.out.println("call method end!");int a = 100;int b = 200;return a + b; //自动装箱(300结果变成Integer)}});// 创建线程对象Thread t = new Thread(task);// 启动线程t.start();// 这里是main方法,这是在主线程中。// 在主线程中,怎么获取t线程的返回结果?// get()方法的执行会导致“当前线程阻塞”Object obj = task.get();System.out.println("线程执行结果:" + obj);// main方法这里的程序要想执行必须等待get()方法的结束// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果// 另一个线程执行是需要时间的。System.out.println("hello world!");}
}

1.8 -线程控制

方法名说明
void yield()使当前线程让步,重新回到争夺CPU执行权的队列中
static void sleep(long ms)使当前正在执行的线程停留指定的毫秒数
void join()等死(等待当前线程销毁后,再继续执行其它的线程)
void interrupt()终止线程睡眠

1.8.1 -sleep()方法 (谁执行谁就是当前线程)

/*** @author Mr.乐* @Description 线程睡眠*/
public class DemoSleep {public static void main(String[] args) {//        创建线程MyThread1 t01 = new MyThread1("黄固");MyThread1 t02 = new MyThread1("欧阳锋");MyThread1 t03 = new MyThread1("段智兴");MyThread1 t04 = new MyThread1("洪七公");//开启线程t01.start();t02.start();t03.start();t04.start();}
}
class MyThread1 extends Thread{public MyThread1() {}public MyThread1(String name) {super(name);}@Override// 重点:run()当中的异常不能throws,只能try catch// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。public void run() {for (int i = 1; i < 50; i++) {System.out.println(this.getName() + "正在打出第 - " + i + "招");try {Thread.sleep(500);//让当前正在执行的线程睡眠指定毫秒数} catch (InterruptedException e) {e.printStackTrace();}}}
}

        注意:run()方法中的异常只能try catch,因为父类没有抛出异常,子类不能抛出比父类更多的异常。 

1.8.2 -interrupt()方法和stop()方法

/*** @author Mr.乐* @Description  终止线程*/
public class DemoInterrupt {public static void main(String[] args) {Thread t = new Thread(new MyRunnable2());t.setName("t");t.start();try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)t.interrupt();
//        t.stop(); //强行终止线程//缺点:容易损坏数据  线程没有保存的数据容易丢失}
}
class MyRunnable2 implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "---> begin");try {// 睡眠1年Thread.sleep(1000 * 60 * 60 * 24 * 365);} catch (InterruptedException e) {
//            e.printStackTrace();}//1年之后才会执行这里System.out.println(Thread.currentThread().getName() + "---> end");}
}

 1.8.3 -合理的终止线程

        做一个boolean类型的标记

/*** @author Mr.乐* @Description*/
public class DemoSleep02 {public static void main(String[] args) {MyRunable4 r = new MyRunable4();Thread t = new Thread(r);t.setName("t");t.start();// 模拟5秒try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}// 终止线程// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。r.run = false;}
}
class MyRunable4 implements Runnable {// 打一个布尔标记boolean run = true;@Overridepublic void run() {for (int i = 0; i < 10; i++){if(run){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}else{// return就结束了,你在结束之前还有什么没保存的。// 在这里可以保存呀。//save....//终止当前线程return;}}}
}

1.8.4 - yield()

暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪之后,有可能还会再次抢到。

/*** @author Mr.乐* @Description 线程让位*/
public class DemoYield {public static void main(String[] args) {//创建线程MyThread5 t01 = new MyThread5("线程01");MyThread5 t02 = new MyThread5("线程02");MyThread5 t03 = new MyThread5("线程03");//开启线程t01.start();t02.start();t03.start();}
}
class MyThread5 extends Thread{public MyThread5() {}public MyThread5(String name) {super(name);}@Overridepublic void run() {for (int i = 0; i < 50; i++) {if(30 == i){Thread.yield();//当循i环到30时,让线程让步//1、回到抢占队列中,又争夺到了执行权//2、回到抢占队列中,没有争夺到执行权}System.out.println(this.getName() + ":" + i);}}
}

 1.8.5 -join()

1.9 - 线程的调度

  • 线程调度模型

    • 均分式调度模型:所有的线程轮流使用CPU的使用权,平均分配给每一个线程占用CPU的时间。

    • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个线程来执行,优先级高的占用CPU时间相对来说会高一点点。

    Java中JVM使用的就是抢占式调度模型

  • getPriority():获取线程优先级

  • setPriority:设置线程优先级

/*** @author Mr.乐* @Description  线程的调度*/
public class Demo07 {public static void main(String[] args) {//创建线程MyThread t01 = new MyThread("线程01");MyThread t02 = new MyThread("线程02");MyThread t03 = new MyThread("线程03");//获取线程优先级,默认是5
//        System.out.println(t01.getPriority());
//        System.out.println(t02.getPriority());
//        System.out.println(t03.getPriority());//设置线程优先级t01.setPriority(Thread.MIN_PRIORITY); //低  - 理论上来讲,最后完成t02.setPriority(Thread.NORM_PRIORITY); //中t03.setPriority(Thread.MAX_PRIORITY); //高  - 理论上来讲,最先完成//开启线程t01.start();t02.start();t03.start();}
}

2 - 线程的安全

2.1 - 数据安全问题

  • 是否具备多线程的环境

  • 是否有共享数据

  • 是否有多条语句操作共享数据

  • 例如:我和小明同时取一个账户的钱,我取钱后数据还没返回给服务器,小明又取了,这个时候小明的余额还是原来的。

  • 如何解决?线程排队执行(不能并发),线程同步机制。

2.1.1 -变量对线程安全的影响

 实例变量:在堆中。

静态变量:在方法区。

局部变量:在栈中。

    以上三大变量中:
        局部变量永远都不会存在线程安全问题。
        因为局部变量不共享。(一个线程一个栈。)
        局部变量在栈中。所以局部变量永远都不会共享。

     实例变量在堆中,堆只有1个。
    静态变量在方法区中,方法区只有1个。
    堆和方法区都是多线程共享的,所以可能存在线程安全问题。

    局部变量+常量:不会有线程安全问题。
    成员变量:可能会有线程安全问题。

 2.1.2 -模拟线程安全问题

public class Test {public static void main(String[] args) {// 创建账户对象(只创建1个)Account act = new Account("act-001", 10000);// 创建两个线程Thread t1 = new AccountThread(act);Thread t2 = new AccountThread(act);// 设置namet1.setName("t1");t2.setName("t2");// 启动线程取款t1.start();t2.start();//t1对act-001取款5000.0成功,余额5000.0//t2对act-001取款5000.0成功,余额5000.0}
}
----------------------------------------------------
public class AccountThread extends Thread {// 两个线程必须共享同一个账户对象。private Account act;// 通过构造方法传递过来账户对象public AccountThread(Account act) {this.act = act;}public void run(){// run方法的执行表示取款操作。// 假设取款5000double money = 5000;// 取款// 多线程并发执行这个方法。act.withdraw(money);System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());}
}
------------------------------------------------
/*** @author Mr.乐* @Description*/
public class Account {// 账号private String actno;// 余额private double balance;public Account() {}public Account(String actno, double balance) {this.actno = actno;this.balance = balance;}public String getActno() {return actno;}public void setActno(String actno) {this.actno = actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}//取款的方法public void withdraw(double money){// t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)// 取款之前的余额double before = this.getBalance(); // 10000// 取款之后的余额double after = before - money;// 在这里模拟一下网络延迟,100%会出现问题try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 更新余额// 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。this.setBalance(after);}
}

2.2 - 线程同步的利弊

  • 好处:解决了线程同步的数据安全问题

  • 弊端:当线程很多的时候,每个线程都会去判断同步上面的这个锁,很耗费资源,降低效率

2.3 -编程模型 

异步编程模型:
            线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
            谁也不需要等谁,这种编程模型叫做:异步编程模型。
            其实就是:多线程并发(效率较高。)

同步编程模型:
            线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
            结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
            两个线程之间发生了等待关系,这就是同步编程模型。
            效率较低。线程排队执行。

2.4 -线程同步

2.4.1 -线程同步方式

        同步语句块:synchronized(this){方法体}  (synchronized括号后的数据必须是多线程共享的数据,才能达到多线程排队)

//        以下代码的执行原理?
//        1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
//        2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
//        找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
//        占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
//        3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面
//        共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
//        直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
//        t2占有这把锁之后,进入同步代码块执行程序。
//
//        这样就达到了线程排队执行。
//        这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
//        执行的这些线程对象所共享的。synchronized (this){double before = this.getBalance();double after = before - money;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);}

        普通同步方法:修饰符 synchronized 返回值类型 方法名(形参列表){方法体}

        synchronized出现在实例方法上,一定锁的是this(此方法)。不能是其他的对象了。 所以这种方式不灵活。

        另外还有一个缺点:synchronized出现在实例方法上, 表示整个方法体都需要同步,可能会无故扩大同步的 范围,导致程序的执行效率降低。所以这种方式不常用。

    public synchronized void withdraw(double money){double before = this.getBalance(); // 10000// 取款之后的余额double after = before - money;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 更新余额this.setBalance(after);

        静态同步方法:修饰符 synchronized static 返回值类型 方法名(形参列表){方法体}

 (静态方法中不能使用this)表示找类锁。类锁永远只有1把。

 2.5 -如何解决线程安全问题

    是一上来就选择线程同步吗?synchronized
        不是,synchronized会让程序的执行效率降低,用户体验不好。
        系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择
        线程同步机制。

    第一种方案:尽量使用局部变量代替“实例变量和静态变量”。  

    第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
    实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
    对象不共享,就没有数据安全问题了。)

    第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
    就只能选择synchronized了。线程同步机制。

2.6 -Lock

        应用场景不同,不一定要在同一个方法中进行解锁,如果在当前的方法体内部没有满足解锁需求时,可以将lock引用传递到下一个方法中,当满足解锁需求时进行解锁操作,方法比较灵活。

   private Lock lock = new ReentrantLock();//定义Lock类型的锁public  void withdraw(double money){// t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)// 取款之前的余额lock.lock();//上锁double before = this.getBalance(); // 10000// 取款之后的余额double after = before - money;// 在这里模拟一下网络延迟,100%会出现问题try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 更新余额// 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。this.setBalance(after);lock.unlock();//解锁}

 2.7 -死锁

形成原因

当两个线程或者多个线程互相锁定的情况就叫死锁

避免死锁的原则

顺序上锁,反向解锁,不要回头

/*** @author Mr.乐* @Description 死锁*/
public class DeadLock {public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();// t1和t2两个线程共享o1,o2Thread t1 = new MyThread1(o1,o2);Thread t2 = new MyThread2(o1,o2);t1.start();t2.start();}
}class MyThread1 extends Thread{Object o1;Object o2;public MyThread1(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2){}}}
}class MyThread2 extends Thread {Object o1;Object o2;public MyThread2(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1){}}}
}

 2.8 -守护线程

java语言中线程分为两大类:
            一类是:用户线程
            一类是:守护线程(后台线程)
            其中具有代表性的就是:垃圾回收线程(守护线程)。

        守护线程的特点:
            一般守护线程是一个死循环,所有的用户线程只要结束,
            守护线程自动结束。
        
        注意:主线程main方法是一个用户线程。

        守护线程用在什么地方呢?
            每天00:00的时候系统数据自动备份。
            这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
            一直在那里看着,每到00:00的时候就备份一次。所有的用户线程
            如果结束了,守护线程自动退出,没有必要进行数据备份了。

public class Demo09 {public static void main(String[] args) {Thread t = new BakDataThread();t.setName("备份数据的线程");// 启动线程之前,将线程设置为守护线程t.setDaemon(true);t.start();// 主线程:主线程是用户线程for(int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}class BakDataThread extends Thread {public void run(){int i = 0;// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。while(true){System.out.println(Thread.currentThread().getName() + "--->" + (++i));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

3 -定时器

定时器的作用:
            间隔特定的时间,执行特定的程序。

在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
            不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持
             定时任务的。 

import java.util.Timer;
import java.util.TimerTask;/*** @author Mr.乐* @Description 定时类*/
public class DemoTimer {public static void main(String[] args) {Timer timer = new Timer();//创建Timer定时器类的对象//匿名内部类timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("我被执行了!~");System.gc();//告诉JVM运行完毕,可以把我回收}},5000);}
}

3.1 -线程与定时器执行轨迹不同

线程与定时器之间互不抢占CPU时间片

import java.util.Timer;
import java.util.TimerTask;/*** @author Mr.乐* @Description 线程与定时器的执行轨迹不同*/
public class DemoTimer {public static void main(String[] args) {new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + "<--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}).start();//定时器实现new Timer().schedule(new TimerTask() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "---" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.gc();//将编程垃圾的定时器进行回收}},5000);}
}

4 -生产者和消费者

4.1 -关于Object类中的wait和notify方法。

        第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象
        都有的方法,因为这两个方式是Object类中自带的。
            wait方法和notify方法不是通过线程对象调用,
            不是这样的:t.wait(),也不是这样的:t.notify()..不对。

 第二:wait()方法作用:
        Object o = new Object();
        o.wait();          

          表示:
            让正在o对象上活动的线程进入等待状态,无期限等待,
            直到被唤醒为止。
            o.wait();方法的调用,会让“当前线程(正在o对象上
            活动的线程)”进入等待状态。

 第三:notify()方法作用:
        Object o = new Object();
        o.notify();

          表示:
              唤醒正在o对象上等待的线程。
          还有一个notifyAll()方法:
              这个方法是唤醒o对象上处于等待的所有线程。

  注意:wait方法和notify方法需要建立在synchronized线程同步的基础之上。

  重点:o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁;

             o.notify()方法只会通知,不会释放之前占有的o对象的锁。

4.2 -生产者和消费者模式

 生产者与消费者模式是并发、多线程编程中经典的设计模式,通过wait和notifyAll方法实现。

例如:生产满了,就不能继续生产了,必须让消费线程进行消费。

           消费完了,就不能继续消费了,必须让生产线程进行生产。

而消费和生产者共享的仓库,就为多线程共享的了,所以需要考虑仓库的线程安全问题。

wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。

wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。

notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。

例1:

/*** @author Mr.乐* @Description 生产者和消费者模式*/
public class wait_notify {public static void main(String[] args) {Box box = new Box();//实例化奶箱类Producer producer = new Producer(box);//生产者对象Customer customer = new Customer(box);//消费者对象Thread tp = new Thread(producer);//创建生产者线程Thread tc = new Thread(customer);//创建消费者线程//启动线程tp.start();tc.start();}
}
//奶箱类
class Box{private int milk;  //放入奶箱中的第几瓶牛奶private boolean state = false; //默认奶箱为空/*** 生产者生产(放)牛奶* @param milk  第几瓶*/public synchronized void put(int milk){if(state){  //true表示奶箱中有牛奶try {wait();  //等待,需要有人唤醒} catch (InterruptedException e) {e.printStackTrace();}}//没有牛奶,需要生产牛奶this.milk = milk;System.out.println("王五将第" + this.milk + "瓶你牛奶放进了奶箱中");this.state = true;//将奶箱状态调整成有牛奶notifyAll();//唤醒全部正在等待的线程}/*** 消费者取牛奶*/public synchronized void get(){if(!state){  //true表示奶箱中有牛奶try {wait();  //等待,需要有人唤醒} catch (InterruptedException e) {e.printStackTrace();}}//有牛奶,需要取牛奶System.out.println("张三将第" + this.milk + "瓶牛奶拿走补了身体!");this.state = false;//将奶箱状态改变成空notifyAll();//唤醒全部正在等待的线程}}
//生产者类
class Producer implements Runnable{private Box b;public Producer(Box b){this.b = b;}@Overridepublic void run() {for (int i = 1; i < 8; i++) {b.put(i);//放牛奶,放几瓶}}
}
//消费者类
class Customer implements Runnable{private Box b;public Customer(Box b){this.b = b;}@Overridepublic void run() {while (true){b.get();//消费者取牛奶}}
}

例2: 

import java.util.ArrayList;
import java.util.List;/*** @author Mr.乐* @Description 生产者和消费者模式02*/
public class ThreadTest16 {public static void main(String[] args) {// 创建1个仓库对象,共享的。List list = new ArrayList();// 创建两个线程对象// 生产者线程Thread t1 = new Thread(new Producer(list));// 消费者线程Thread t2 = new Thread(new Consumer(list));t1.setName("生产者线程");t2.setName("消费者线程");t1.start();t2.start();}
}// 生产线程
class Producer implements Runnable {// 仓库private List list;public Producer(List list) {this.list = list;}@Overridepublic void run() {// 一直生产(使用死循环来模拟一直生产)while(true){// 给仓库对象list加锁。synchronized (list){if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。try {// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。list.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 程序能够执行到这里说明仓库是空的,可以生产Object obj = new Object();list.add(obj);System.out.println(Thread.currentThread().getName() + "--->" + obj);// 唤醒消费者进行消费list.notifyAll();}}}
}// 消费线程
class Consumer implements Runnable {// 仓库private List list;public Consumer(List list) {this.list = list;}@Overridepublic void run() {// 一直消费while(true){synchronized (list) {if(list.size() == 0){try {// 仓库已经空了。// 消费者线程等待,释放掉list集合的锁list.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 程序能够执行到此处说明仓库中有数据,进行消费。Object obj = list.remove(0);System.out.println(Thread.currentThread().getName() + "--->" + obj);// 唤醒生产者生产。list.notifyAll();}}}
}

5 -线程池

5.1 - 概念

线程池就是首先创建一些线程,他们的集合称之为线程池。线程池在系统启动时会创建大量空闲线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁(死亡),而是再次返回到线程池中成为空闲状态,等待执行下一个任务。

5.2 - 线程池的工作机制

在线程池的编程模式下,任务是分配给整个线程池的,而不是直接提交给某个线程,线程池拿到任务后,就会在内部寻找是否有空闲的线程,如果有,则将任务交个某个空闲线程。

5.3 - 使用线程池的原因

多线程运行时,系统不断创建和销毁新的线程,成本非常高,会过度的消耗系统资源,从而可能导致系统资源崩溃,使用线程池就是最好的选择。

5.4 - 可重用线程

方法名说明
Executors.newCacheThreadPoll();创建一个可缓存的线程池
execute(Runnable run)启动线程池中的线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author Mr.乐* @Description  可重用线程池*/
public class ExecutorsTest {public static void main(String[] args) {//创建线程池ExecutorService threadPoll = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {//如果不睡眠,那么第一个执行完的线程无法及时成为空闲线程,那么线程池就会让一个新的线程执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//每次循环都会开启一个线程threadPoll.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "正在被执行!~");}});}threadPoll.shutdown();//关闭线程池//线程池是无限大,当执行当前任务时,上一个任务已经完成,会重复执行上一个任务的线程,而不是每次使用新的线程}
}

6 -多线程并发的线程安全问题

        了解了线程池,接下来从底层讲一下多线程并发的安全问题。 

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author Mr.乐* @Description  并发安全*/
public class MyTest {//定义静态变量static int a=0;static int count=2000;public static void main(String[] args) {//创建线程池ExecutorService service = Executors.newCachedThreadPool();for(int i=0;i<count;i++){service.execute(new Runnable() {@Overridepublic void run() {a++;}});}关闭线程池service.shutdown();System.out.println(a);//1987}
}

         以上程序运行并没有达到预期的2000,此处多线程并发,a共享,所以没达到2000

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author Mr.乐* @Description  并发安全*/
public class MyTest {static int a=0;static int count=2000;public static void main(String[] args) throws InterruptedException {ExecutorService service = Executors.newCachedThreadPool();//闭锁 在一些条件下可放开  参数:加多少把锁CountDownLatch countDownLatch=new CountDownLatch(count);for(int i=0;i<count;i++){service.execute(new Runnable() {@Overridepublic void run() {a++;//解一把锁countDownLatch.countDown();}});}service.shutdown();//会进入阻塞状态  什么时候把锁全解了   阻塞状态才会解除countDownLatch.await();System.out.println(a);//1987}
}

         此处所用的加锁方法也没有实现预期效果。

6.1 -CPU多级缓存

        打开任务管理器,在性能中可查看CPU的多级缓存。

        程序进程中的数据,都在内存中存着。 而CPU缓存,是为了解决内存没有CPU快的问题。当一个数据需要CPU修改,而内存无法及时给CPU返回数据,就会拖慢CPU的运行速度。所以有了CPU缓存。

        当CPU需要在内存中读数据时,在时间局部性上(不久的将来)还得读此数据。,将此数据放在CPU缓存中。

        当用到内存中数据(例如 a)时,而数据旁边的数据(例:static int a=0;   int b=0;   用a时b为旁边的数据)在空间局部性上,会用到相邻的数据(例如 b),CPU也会读到b,将b数据放在CPU缓存中。

        当CPU读取数据时,会让CPU缓存同步内存中的数据。然后CPU缓存中的数据再交给CPU去修改。当CPU修改完后,会把修改的数据传给CPU缓存(此时CPU不需要等待),再由CPU缓存传给内存 。 

当CPU 01将数据修改完后,CPU缓存01还没有将数据传给内存,CPU缓存02读到了a,此时a的值为0。

        以下为线程安全的两种方式。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author Mr.乐* @Description  并发安全 synchronized*/
public class MyTest {static int a=0;static int count=2000;public static void main(String[] args) throws InterruptedException {ExecutorService service = Executors.newCachedThreadPool();//闭锁 在一些条件下可放开  参数:加多少把锁CountDownLatch countDownLatch=new CountDownLatch(count);for(int i=0;i<count;i++){service.execute(new Runnable() {@Overridepublic void run() {synchronized (MyTest.class) {a++;//解一把锁countDownLatch.countDown();}}});}service.shutdown();//会进入阻塞状态  什么时候把锁全解了   阻塞状态才会解除countDownLatch.await();System.out.println(a);//2000}
}
-------------------------------------------------------------------
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;/*** @author Mr.乐* @Description  并发安全 synchronized*/
public class MyTest {static int a=0;static int count=2000;public static void main(String[] args) throws InterruptedException {ExecutorService service = Executors.newCachedThreadPool();//闭锁 在一些条件下可放开  参数:加多少把锁CountDownLatch countDownLatch=new CountDownLatch(count);//信号量Semaphore semaphore=new Semaphore(1);for(int i=0;i<count;i++){service.execute(new Runnable() {@Overridepublic void run() {try {  //拿走一个信号semaphore.acquire();a++;//解一把锁countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}finally {//释放信号semaphore.release();}}});}service.shutdown();//会进入阻塞状态  什么时候把锁全解了   阻塞状态才会解除countDownLatch.await();System.out.println(a);//2000}
}

7 -总结

        以上就是我对多线程初级的所有总结,希望对大家有所帮助。


http://chatgpt.dhexx.cn/article/fQcUtC4K.shtml

相关文章

JAVA线程

一、线程相关概念 &#xff08;一&#xff09;程序、进程和线程的区别 程序 程序是含有指令和数据的文件&#xff0c;被存储在磁盘或其他的数据存储设备中&#xff0c;也就是说程序是静态的代码。 进程 进程是程序的一次执行过程&#xff0c;是系统运行的基本单位&#xf…

Java 线程 基础知识总结

线程基础 很不严谨的说&#xff0c;线程是什么&#xff1f;线程就是为了让很多个东西并发执行&#xff0c;大大的提高程序执行的效率啊 三个非常重要的概念&#xff1a; 程序&#xff1a;一组写好了的静态代码块&#xff08;就我们写的那些代码玩意&#xff09;进程&#xf…

Java多线程(超详解)

目录 1. 线程简介 1.1 程序 1.2 进程 1.3 线程 1.4 多线程 1.5 普通方法调用和多线程 2. 线程创建 2.1 继承Thread类 2.2 实现Runnable接口 2.3 实现Callable接口&#xff08;了解&#xff09; 2.4 网图下载 2.4.1 通过继承Thread类实现网图下载 2.4.2 通…

java 线程详解

一、线程的基本概念 一个程序最少需要一个进程&#xff0c;而一个进程最少需要一个线程。关系是线程–>进程–>程序的大致组成结构。所以线程是程序执行流的最小单位&#xff0c;而进程是系统进行资源分配和调度的一个独立单位。 一个线程就是在进程中的一个单一的顺序…

JAVA多线程详解(超详细)

目录 一、线程简介1、进程、线程2、并发、并行、串行3、进程的三态 二、线程实现1、继承Thread类2、实现Runnable接口3、实现Callable接口&#xff08;不常用&#xff09; 三、线程常用方法1、线程的状态2、线程常用方法 四、多线程1、守护&#xff08;Deamon&#xff09;线程2…

Java多线程(超详细!)

1、什么是进程&#xff1f;什么是线程&#xff1f; 进程是:一个应用程序&#xff08;1个进程是一个软件&#xff09;。 线程是&#xff1a;一个进程中的执行场景/执行单元。 注意&#xff1a;一个进程可以启动多个线程。 eg. 对于java程序来说&#xff0c;当在DOS命令窗口中…

count/count if函数的基本用法

count函数&#xff0c;用来计算单元格的数的个数&#xff0c;只是用来计数&#xff0c;并且只有只记录数子的个数&#xff0c;文本的个数是不被记录的。 但是很少会用到单纯的count函数&#xff0c;往往在工作中计数是带有条件的。就会用到countif函数 COUNTIF函数需要注意的点…

EXCEL COUNTIF()的一些奇特的用法

文章目录 前言一、统计第几次重复二、统计不重复的数量三、通配符模糊统计四、防止重复录入五、忽略错误值或空值统计六、重复值填充背景色总结 前言 日常工作中需要度娘很多知识点或者方法&#xff0c;但每次用了就忘&#xff0c;下次遇到就需要继续度娘&#xff0c;故在此记…

Excel多条件计数——COUNTIFS【获奖情况统计】

问题描述 当前&#xff0c;我们需要对表格中的获奖情况进行统计 奖励级别&#xff1a;院级、校级、国家级、国际级奖励内容&#xff1a;特等奖、一等奖、二等奖、三等奖、优胜奖 功能要求 对所有奖励级别进行统计根据级别&#xff0c;计算内容数量 当有人的选项内容如下时 …

如何在Microsoft Excel中使用COUNTIF函数

COUNTIF 是一个 Excel 函数,用于对满足单个条件的区域中的单元格进行计数。COUNTIF可用于计算包含日期、数字和文本的单元格。COUNTIF 中使用的条件支持逻辑运算符(>、<、<>、=)和通配符(*、?)进行部分匹配。 例如,我们想计算包含 Google或 Facebook 的单元…

COUNT函数的使用

一、问题描述 今天在随手练习sql的时候&#xff0c;发现count查出来的数量和实际的数量不对&#xff0c;下面是我查询的sql 我想看看suitName字段一共有多少种数据 SELECTCOUNT(DISTINCT suitName) AS suitNameNum FROMrace_goods 得到的结果是suitName条数是22条&#xff0c…

countif是什么意思,如何运用?

countif是什么意思&#xff1f;countif函数是excel中对指定区域中符合指定条件的单元格计数的一个函数&#xff0c;简单来说就是算出某个参数的数量。那么&#xff0c;countif函数具体是如何运用的呢&#xff1f;小编分为两种情景为大家总结了操作步骤。 情景一&#xff1a;计算…

excel通过sumproduct和countifs不重复计数(数据中包含空白单元)

1. 常规情况&#xff0c;数据中不包含空白单元格&#xff0c;如下图&#xff1a; SUMPRODUCT((A2:A24E2)*(B2:B24F2)*(1/COUNTIFS(A2:A24,A2:A24,B2:B24,B2:B24,C2:C24,C2:C24))) 2. 数据中包含空白单元格&#xff0c;如下图&#xff1a; 数据中包含空白单元的情况&#xff0c…

countif怎么读(countif怎么读)

SUM,AVERAGE,MAX,MIN,PRODUCT,CUONT,RANK,IF,SUMIF,COUNTIF怎么读呀&#xff01;谢谢 SUM(桑母) 求和函数&#xff1b; AVERAGE(哎五瑞之) 求平均数函数&#xff1b; MAX(麦克斯) 最大值函数&#xff1b; MIN(民) 最小值函数&#xff1b; PRODUCT(普若达克特) 求积函数&#xf…

[Excel常用函数] countif countifs函数

countif函数 1.countif函数的含义 在指定区域中按指定条件对单元格进行计数&#xff08;单条件计数&#xff09; 2.countif函数的语法格式 countif&#xff08;range&#xff0c;criteria&#xff09; 参数range 表示条件区域——对单元格进行计数的区域。 参数criteria …

Microsoft Excel 教程:如何在 Excel 中使用 COUNTIF 函数?

欢迎观看 Microsoft Excel 教程&#xff0c;小编带大家学习 Microsoft Excel 的使用技巧&#xff0c;了解如何在 Excel 中使用 COUNTIF 函数。 COUNTIF 是一个统计函数&#xff0c;用于统计满足某个条件的单元格的数量&#xff1b;例如&#xff0c;某个姓名在单元格区域中出现…

[Excel函数] COUNT函数 | COUNTIF函数 | COUNTIFS函数

1.COUNT函数 语法: COUNT(value1,[value2],...) COUNT函数用于计算区域中包含数字的单元格的个数 (不统计文本性数字) COUNT函数很少单独使用&#xff0c;一般和其他函数嵌套使用 案例: 统计数字的个数 注意: A13该单元格数字为文本格式&#xff0c;COUNT函数不统计文本性数字 …

Excel如何使用COUNTIF函数

COUNTIF函数是我们在工作中经常使用的函数&#xff0c;今天就给大家分享一下如何使用countif函数。 1、COUNTIF函数主要用于对区域中满足单个指定条件的单元格进行计数。它的语法结构是COUNTIF(统计区域&#xff0c;统计条件) 注意要点 1.COUNTIF不区分大小写。 2.在条件中可…

sum与countif、countifs函数套用

1、sum与countif套用&#xff0c;实现多条件计数求和 公式&#xff1a;sum(countif(条件区域,{"条件1","条件2","条件3"})) 示例&#xff1a; 2、sum与countifs套用&#xff0c;实现多区域多条件计数求和 公式&#xff1a;sum(countifs(条件区…

COUNTIFS函数使用

COUNTIFS函数是Excel中的一个统计函数&#xff0c;用来计算多个区域中满足给定条件的单元格的个数&#xff0c;可以同时设定多个条件。 countifs(criteria_range1,criteria1,criteria_range2,criteria2,…) 参数说明 criteria_range1&#xff1a;为第一个需要计算其中满足某个…