java多线程(超详细)

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://www.mzph.cn/news/75844.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无涯教程-JavaScript - OCT2HEX函数

描述 OCT2HEX函数将八进制数转换为十六进制。 语法 OCT2HEX (number, [places])争论 Argument描述Required/OptionalNumber 您要转换的八进制数。 数字不得超过10个八进制字符(30位)。数字的最高有效位是符号位。其余的29位是幅度位。 负数使用二进制补码表示。 RequiredPl…

C# OpenCvSharp 通道分离

效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using OpenCvSharp; using OpenCvSharp.Extensions;namespac…

平板触控笔哪款好用?好用的第三方apple pencil

而对于那些把ipad当做学习工具的人而言&#xff0c;苹果Pencil就成了必备品。但因为苹果Pencil太贵了&#xff0c;不少的学生们买不起。因此&#xff0c;最佳的选择还是平替电容笔&#xff0c;今天在这里整理了一些高性价比的电容笔&#xff01; 一、挑选电容笔的要点&#xf…

springboot之二:整合junit进行单元测试+整合redis(本机、远程)+整合mybatis

资源地址&#xff1a; 整合junit的代码&#xff1a;https://download.csdn.net/download/zhiaidaidai/88291527 整合redis的代码&#xff1a;https://download.csdn.net/download/zhiaidaidai/88291536 整合mybatis的代码&#xff1a;https://download.csdn.net/download/zh…

零基础教程:使用yolov8训练无人机VisDrone数据集

1.准备数据集 1.先给出VisDrone2019数据集的下载地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1e2Q0NgNT-H-Acb2H0Cx8sg 提取码&#xff1a;31dl 2.将数据集VisDrone放在datasets目录下面 2.数据集转换程序 1.在根目录下面新建一个.py文件&#xff0c;取名叫…

使用POI实现操作Excel文件。

1、添加依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-o…

[keil] uv编译分析

假设Keil安装路径: C:\Keil_v5\ 假设工程在 d:\HELLO , 工程Targets名:Simulator [在Manage Project Items中可修改] 如下指令为:Build(F7) C:\Keil_v5\UV4\UV4.exe -b d:\HELLO\Hello.uvproj -j0 -t Simulator -o d:\HELLO\uv4.log 如下指令为:Rebuild(CtrlAltF7) C:\Kei…

探究SpringWeb对于请求的处理过程

探究目的 在路径归一化被提出后&#xff0c;越来越多的未授权漏洞被爆出&#xff0c;而这些未授权多半跟spring自身对路由分发的处理机制有关。今天就来探究一下到底spring处理了什么导致了才导致鉴权被绕过这样严重的问题。 DispatcherServlet介绍 首先在分析spring对请求处…

[刷题记录]牛客面试笔刷TOP101

牛客笔试算法必刷TOP101系列,每日更新中~(主要是记录自己的刷题,所以描述的可能不是很清楚 但如果刚好能帮助到你就更好了) 后续后头复习的时候,记得是看正解啊,别对着错的例子傻傻看了... 目录 1.合并有序链表2023.9.3 2.链表是否有环2023.9.4 3.判断链表中环的入口点 …

一分钟图情论文:《原始的布拉德福定律》

天津大学图书馆的研究馆员范铮先生&#xff0c;在《图书情报工作》第一期中发表了题为《原始的布拉德福定律》的文章&#xff0c;详细介绍了布拉德福定律的历史背景、调查统计数据、文献曲线以及理论推导等关键内容。这篇文章让我们能够深入了解布拉德福定律的本质和原始构想。…

概率论与数理统计学习笔记(7)——全概率公式与贝叶斯公式

目录 1. 背景2. 全概率公式3. 贝叶斯公式 1. 背景 下图是本文的背景内容&#xff0c;小B休闲时间有80%的概率玩手机游戏&#xff0c;有20%的概率玩电脑游戏。这两个游戏都有抽卡环节&#xff0c;其中手游抽到金卡的概率为5%&#xff0c;端游抽到金卡的概率为15%。已知小B这天抽…

Windows环境下Springboot3+Graalvm+Idea 打包成原生镜像 踩坑

https://github.com/oracle/graal/https://github.com/graalvm/graalvm-ce-builds/releases/对应关系graalvm-ce-java17-windows-amd64-X.X.X.zipnative-image-installable-svm-java17-windows-amd64-X.X.X.jar本人使用:graalvm-ce-java17-windows-amd64-23.0.1.zipnative-imag…

蓝桥杯官网练习题(纸牌三角形)

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 A,2,3,4,5,6,7,8,9 共 99 张纸牌排成一个正三角形&#xff08;A 按 1 计算&#xff09;。要求每个边的和相等。 下图就是一种排法。 这样的排法可能会有很多。 如果…

修改Docker镜像默认下载地址

1、安装完docker desktop后&#xff0c;先不要打开 2、新建目录 D:\ProgramData\Docker 3、在C:\Users\你的用户名\AppData\Local下&#xff0c;打开cmd或者powershell执行以下命令&#xff0c;命令语法略有不同。 powershell命令&#xff1a; cmd /c mklink /J Docker D:\Pro…

1-5 AUTOSAR数据交换文件ARXML

目录 一、Arxml文件 二、各类ARXML文件 一、Arxml文件 arxml文件是AUTOSAR&#xff08;Automotive Open System Architecture&#xff09;标准定义的XML文件&#xff0c;用于描述汽车电子系统中的软件组件、通信接口和参数配置等信息。 arxml文件的主要作用是在AUTOSAR架构下…

golang教程 beego框架笔记一

安装beego 安装bee工具 beego文档 # windos 推荐使用 go install github.com/beego/bee/v2master go get -u github.com/beego/bee/v2masterwindows使用安装bee工具时碰到的问题&#xff1b; 环境配置都没有问题&#xff0c;但是执行官网的命令&#xff1a;go get -u github…

打造高效的私密论坛网站:Cpolar内网穿透+HadSky轻量级搭建指南

文章目录 前言1. 网站搭建1.1 网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3 Cpolar稳定隧道&#xff08;本地设置&#xff09;2.4 公网访问测试 总结 前言 经过多年的基础…

大数据和数据要素有什么关系?

大数据与数据要素之间存在密切的关系。大数据是指海量、多样化、高速生成的数据&#xff0c;而数据要素是指构成数据的基本元素或属性。数据要素包括但不限于数据的类型、结构、格式、单位、精度等。 大数据的产生和应用离不开数据要素的支持。数据要素确定了数据的基本特征和…

【网络基础】——HTTPS

目录 HTTPS背景知识 HTTPS是什么&#xff1f; 加密解密 为什么要加密 常见的加密方式 对称加密 非对称加密 数据摘要&&数据指纹 数字签名 HTTPS工作过程探究 方案1&#xff1a;只使用对称加密 方案2&#xff1a;只使用非对称加密 方案3&#xff1a;双方…

conda和Python的虚拟环境如何结合使用,以及二者之间到底有什么区别?

问题描述 今天在复现streamlit的代码时&#xff08;参考Streamlit 讲解专栏&#xff08;一&#xff09;&#xff1a;安装以及初步应用&#xff09;&#xff0c;根据这篇博文指导&#xff0c;要先用以下指令创建一个虚拟环境&#xff1a; # 创建虚拟环境&#xff08;使用venv&a…