多线程
文章目录
多线程一、多线程概念1. 进程(Process)与线程(Thread)的概念 二、线程创建的方式1. 第一种方式:继承Thrad类2. 第二种方式(实现Runnable接口)3. 两种方式的对比4. 第三种方式:实现Callable接口(了解即可)5.静态代理与Thread的关系 三、Lamda表达式(扩充知识点)1. 什么是lambda表达式2. 为什么要用lambda表达式3. 理解函数式接口4. 从对象方法调用到lambda的推导 四、线程状态1. 线程五大状态2.线程方法3.停止线程4. 线程休眠5. 线程礼让6.Join合并线程7. 线程状态观测(Thread.State,具体可查看JDK帮助文档)8. 线程的优先级9. 守护(daemon)线程 五、线程同步(重点)1. 概念2.多线程可能出现的问题3. 队列和锁(synchronized)4. 同步方法5. 同步块6. 总结 六、死锁1. 概念2. 死锁避免的条件3. Lock锁4. synchronized与Lock的对比 七、生产者与消费者1. 线程通信2. 解决方式1——管程法3. 解决方式2——信号灯法 八、线程池1. 使用线程池 九、面试题拓展1. 问:常听人说,在32位的机器上对long型变量进行加减操作存在并发隐患,到底是不是这样呢?2.JUC并发包的理解3. lock锁的创建一、多线程概念
1. 进程(Process)与线程(Thread)的概念
说起进程,就不得不说下程序。程序是指令和数据的有序集合,具本身没有仕何运行的含义,是一个静态的概念。而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位。注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
线程:main函数即主线程,gc垃圾回收线程
进程:程序编写后处于静态,运行起来的程序即进程,进程执行的过程中会去调度线程。
线程就是独立的执行路径;
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;main()称之为主线程,为系统的入口,用于执行整个程序;
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制线程会带来额外的开销,如cpu调度时间,并发控制开销。
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致(并发问题)
线程执行start方法时,新的线程开始执行,这时gvm虚拟机会自动去执行run()方法,详细介绍见Tread源码。
二、线程创建的方式
1. 第一种方式:继承Thrad类
分析:
案例:图片下载
//下载器class WebDownloader{//下载方法public void downloader(String url, String name){try {FileUtils.copyURLToFile(new URL(url),new File(name));} catch (IOException e) {e.printStackTrace();System.out.println("IO异常,downloader方法出现问题");}}}
2. 第二种方式(实现Runnable接口)
注意:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
3. 两种方式的对比
4. 第三种方式:实现Callable接口(了解即可)
实现Callable接口,需要返回值类型重写call方法,需要抛出异常创建目标对象创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);提交执行:Future result1 = ser.submit(t1);获取结果: boolean r1 = result1.get()关闭服务: ser.shutdownNow();package com.wedu.TreadDemo.callable;import java.util.concurrent.*;public class TestCallable {public static void main(String[] args) throws ExecutionException, InterruptedException {ThreadImplCallable tCallable = new ThreadImplCallable("call");//1.创建执行服务ExecutorService es = Executors.newFixedThreadPool(1);//2.执行提交Future r1 = es.submit(tCallable);//3.获取结果Object o = r1.get();//4.关闭服务es.shutdown();}}class ThreadImplCallable<T> implements Callable<T> {/*** 线程名称*/private String threadName;/*** 构造函数* @param threadName 线程名称*/public ThreadImplCallable(String threadName) {this.threadName = threadName;}/*** 重写call方法*/@Overridepublic T call() throws Exception {System.out.println(threadName);return (T)threadName;}}
运行结果:
Callable的好处:
1.可以定义返回值
2.可以抛出异常
5.静态代理与Thread的关系
静态代理有如下要素:
1.目标角色(真实角色)。
2.代理角色。
3.目标角色和代理角色实现同一接口。
4.代理角色持有目标角色的引用。
下面的例子是一个简单的静态代理模式:
/*** 统一接口* * @author Administrator* */public interface Callable {void call();}/*** 目标角色* * @author Administrator* */public class Target implements Callable {public void call() {System.out.println("call...");}}/*** 代理角色* * @author Administrator* */public class Proxy implements Callable {//被代理的角色Callable callable;public Proxy(Callable callable) {this.callable = callable;}public void call() {System.out.println("before...");callable.call();System.out.println("after...");}}/****测试*/public class StaticProxyApp {public static void main(String[] args) {Callable target = new Target();Callable proxy = new Proxy(target);proxy.call();}}
输出:
before…
call…
after…
JDK中最典型的静态代理就是线程的使用了:
public class ThreadStaticProxy {public static void main(String[] args) {Runnable target = new MyTarget();// 目标角色Thread proxy = new Thread(target);// 代理角色proxy.start();}}class MyTarget implements Runnable {public void run() {System.out.println("run...");}}
线程体(也就是我们要执行的具体任务)实现了Runnable接口和run方法。同时Thread类也实现了Runnable接口。此时,线程体就相当于目标角色,Thread就相当于代理角色。当程序调用了Thread的start()方法后,Thread的run()方法会在某个特定的时候被调用。thread.run()方法:
public void run() {if (target != null) {target.run();}}
实际上是执行了线程体的代码。
三、Lamda表达式(扩充知识点)
1. 什么是lambda表达式
2. 为什么要用lambda表达式
避免匿名内部类定义过多可以让你的代码看起来很简洁去掉了一堆没有意义的代码,只留下核心的逻辑3. 理解函数式接口
理解Functional Interface(函数式接口)是学习Java8 lambda表达式的关键所在。
函数式接口的定义:
任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
public interface Runnable {public abstract void run();}
对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。
4. 从对象方法调用到lambda的推导
package com.wedu.TreadDemo;//定义一个函数式接口interface ILike {void lambda();}//实现类class Like implements ILike {@Overridepublic void lambda() {System.out.println("i like lambda");}}public class TestLambda1 {//静态内部类static class Like2 implements ILike {@Overridepublic void lambda() {System.out.println("i like lambda2");}public static void main(String[] args) {//1.对象方法调用ILike like = new Like();like.lambda();//2.静态内部类调用like = new Like2();like.lambda();//3.局部内部类class Like3 implements ILike {@Overridepublic void lambda() {System.out.println("i like lambda3");}}like = new Like3();like.lambda();//4. 匿名内部类,没有类的名称,必须借助接口或者父类like = new ILike() {@Overridepublic void lambda() {System.out.println(" i like lambda4");}};like.lambda();//5.用lambda简化,省去接口和方法like = () ->{System.out.println("i like lambda5");};like.lambda();}}}
四、线程状态
1. 线程五大状态
2.线程方法
3.停止线程
不推荐使用JDK提供的stop()、destroy()方法。【已废弃】
推荐线程自己停止下来
建议使用一个标志位进行终止变量当flag=false,则终止线程运行。
package com.wedu.TreadDemo;public class TestStop implements Runnable {//1.线程中定义线程体使用的标识private boolean flag = true;@Overridepublic void run() {//2. 线程体使用该标识while (flag) {System.out.println("run. . . Thread");}}//3.对外提供方法改变标识public void stop (){this.flag = false;}//主线程和子线程交替执行public static void main(String[] args) {TestStop testStop = new TestStop();new Thread(testStop).start();for (int i = 0; i < 1000; i++) {System.out.println("main"+i);if (i==900){//调用stop方法切换标志位,让线程停止testStop.stop();System.out.println("线程该停止了");}}}}
运行结果:子线程停止,主线程继续执行。
4. 线程休眠
sleep (时间)指定当前线程阻塞的毫秒数;sleep存在异常 InterruptedException;sleep时间达到后线程进入就绪状态;sleep可以模拟网络延时,倒计时等;每一个对象都有一个锁,sleep不会释放锁。5. 线程礼让
礼让线程,让当前正在执行的线程暂停,但不阻塞将线程从运行状态转为就绪状态让cpu重新调度,礼让不一定成功!看CPU心情注意:线程A和线程B,线程A在执行的过程中,触发礼让事件,是回到就绪状态,这时线程A、B都处于就绪状态,cpu重新调度两个线程,线程A不一定会先执行,所以会出现礼让失败的现象。
package com.wedu.TreadDemo;//测试礼让线程//礼让不一定成功,看cPU心情public class TestYield {public static void main(String[] args) {MyYield myYield = new MyYield( );new Thread(myYield,"a").start( );new Thread(myYield,"b").start( );}}class MyYield implements Runnable{@Overridepublic void run( ) {System.out.println(Thread.currentThread( ) .getName()+"线程开始执行");Thread.yield();//礼让System.out.println( Thread.currentThread( ).getName()+"线程停止执行");}}
6.Join合并线程
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞可以想象成插队package com.wedu.TreadDemo;public class TestJoin implements Runnable {public static void main(String[] args) throws InterruptedException {TestJoin testJoin = new TestJoin();Thread thread = new Thread(testJoin);thread.start();for (int i = 0; i < 100; i++) {if (i == 50)thread.join();//线程阻塞System.out.println("main" + i);}}@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("join"+i);}}}
结果:当执行到线程阻塞时,主线程停止,直到子线程执行完毕再执行主线程。
7. 线程状态观测(Thread.State,具体可查看JDK帮助文档)
线程可以处于以下状态之一:
NEW
尚未启动的线程处于此状态。
RUNNABLE
在Java虚拟机中执行的线程处于此状态。
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
WAITING
正在等待另一个线程执行特定动作的线程处于此状态。
TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
TERMINATED
已退出的线程处于此状态。
一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。
package com.wedu.TreadDemo;//观察测试线程的状态public class TestState {public static void main(String[] args) {Thread thread = new Thread(()->{for (int i = 0; i < 5; i++) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("休息结束!");});//创建线程状态Thread.State state = thread.getState();System.out.println(state);//NEW//线程启动状态thread.start();state = thread.getState();System.out.println(state);//RUNNABLEwhile (state !=thread.getState().TERMINATED){//线程不休眠就一直输出状态try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}state = thread.getState();//更新线程状态System.out.println(state);}}}
结果:
8. 线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。线程的优先级用数字表示,范围从1~10. Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10;Thread.NORM_PRIORITY = 5; 使用以下方式改变或获取优先级 getPriority() . setPriority(int xxx)注意:优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度,但是一般不会出现‘性能倒置’的情况。
package com.wedu.TreadDemo;public class TestPriority {public static void main(String[] args) {//主线程的优先级System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());MyThread myThread = new MyThread();Thread thread = new Thread(myThread);Thread thread1 = new Thread(myThread);Thread thread2 = new Thread(myThread);Thread thread3 = new Thread(myThread);//线程默认优先级(先设置优先级再启动)thread.start();thread1.setPriority(1);thread1.start();thread2.setPriority(8);thread2.start();thread3.setPriority(Thread.MAX_PRIORITY);thread3.start();}}class MyThread implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());}}
结果:看cpu心情
9. 守护(daemon)线程
线程分为用户线程(main)和守护线程(gc)虚拟机必须确保用户线程执行完毕虚拟机不用等待守护线程执行完毕常见守护线程有:后台记录操作日志,监控内存,垃圾回收等待…通过Thread.setDaemon(false)设置为用户线程
通过Thread.setDaemon(true)设置为守护线程
线程属性的设置要在线程启动之前,否则会报IllegalThreadStateException异常
用户线程和守护线程的区别:
1.主线程结束后用户线程还会继续运行,JVM存活
2.如果没有用户线程,都是守护线程,那么JVM结束(所有的线程都会结束)
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。那Java的守护线程是什么样子的呢?当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出。
守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程都是守护线程。与之对应的是用户线程,用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出。
注意:线程创建的时候默认都是用户线程,可以通过 Thread 的 setDaemon 方法修改值为true,改为守护线程。
五、线程同步(重点)
1. 概念
现实生活中,我们会遇到 “同一个资源,多个人都想使用“ 的问题,比如:食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队,一个个来。
处理多线程问题时,多个线程访问同一个对象==(并发问题)==,并且某些线程还想修改这个对象。这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
1、synchronized关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象;
3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
2.多线程可能出现的问题
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
package com.wedu.TreadDemo.syn;public class UnsafeBuyTicket {public static void main(String[] args) {BuyTicket buyTicket = new BuyTicket();new Thread(buyTicket, "我").start();new Thread(buyTicket, "你们").start();new Thread(buyTicket, "他").start();}}class BuyTicket implements Runnable {//票private int ticketNums = 10;boolean flag = true;//外部停止方式@Overridepublic void run() {//买票while (flag) {try {buy();} catch (InterruptedException e) {e.printStackTrace();}}}private void buy() throws InterruptedException {//判断是否有票if (ticketNums <= 0) {flag = false;return;}//模拟延时Thread.sleep(100);//买票System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);}}
运行结果:
3. 队列和锁(synchronized)
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题(安全但是性能低):
①一个线程持有锁会导致其他所有需要此锁的线程挂起;
②在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
③如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
4. 同步方法
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法 和 synchronized 块。
同步方法:public synchronized void method(int args){ }
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
缺陷:若将一个大的方法申明为synchronized将会影响效率
5. 同步块
同步块:synchronized**(Obj){ }**
Obj称之为同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身
同步监视器的执行过程
第一个线程访问,锁定同步监视器,执行其中代码;
第二个线程访问,发现同步监视器被锁定,无法访问;
第一个线程访问完毕,解锁同步监视器;
第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
package com.wedu.TreadDemo.syn;public class UnsafeBuyTicket {public static void main(String[] args) {BuyTicket buyTicket = new BuyTicket();new Thread(buyTicket, "我").start();new Thread(buyTicket, "你们").start();new Thread(buyTicket, "他").start();}}class BuyTicket implements Runnable {//票private int ticketNums = 10;boolean flag = true;//外部停止方式@Overridepublic void run() {//买票while (flag) {try {buy();} catch (InterruptedException e) {e.printStackTrace();}}}//synchronized 同步方法,锁的是thisprivate synchronized void buy() throws InterruptedException {//判断是否有票if (ticketNums <= 0) {flag = false;return;}//模拟延时Thread.sleep(100);//买票System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);}}
运行结果:
注意锁的对象就是变化的量,需要增删改的对象:
怎么理解,我们看下一个案例,在这个取钱的案例里面,我一开始锁的是银行,没有锁成功,后面改成了锁账户,成功了。
最初没有加锁的情况:
package com.wedu.TreadDemo.syn;//不安全的取钱方式//两个人去银行取钱public class UnsafeBank {public static void main(String[] args) {//账户Account account = new Account(100,"总额度");Bank myself = new Bank(account, 50, "myself");Bank Wife = new Bank(account, 100, "myWife");myself.start();Wife.start();}}class Account{int money; //余额String name; //卡名public Account(int money, String name) {this.money = money;this.name = name;}}//银行,模拟取款class Bank extends Thread{Account account;//账户int drawingMoney;//取了多少钱int nowMoney;//现在手里还有多少钱public Bank(Account account, int drawingMoney, String name) {super(name);this.account = account;this.drawingMoney = drawingMoney;}//取钱@Overridepublic void run() {//判断卡里钱还够不够if (account.money-drawingMoney<0){System.out.println(Thread.currentThread().getName()+"钱不够了,取不了");return;}//sleep可以放大问题的发生性try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//卡里余额 = 余额 - 取的钱account.money = account.money-drawingMoney;//手里的钱nowMoney = drawingMoney+nowMoney;System.out.println(account.name+"余额为"+account.money);System.out.println(this.getName()+"手里的钱:"+nowMoney);}}
运行结果:
使用同步方法锁银行
//取钱@Overridepublic synchronized void run() {//判断卡里钱还够不够if (account.money-drawingMoney<0){System.out.println(Thread.currentThread().getName()+"钱不够了,取不了");return;}//sleep可以放大问题的发生性try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//卡里余额 = 余额 - 取的钱account.money = account.money-drawingMoney;//手里的钱nowMoney = drawingMoney+nowMoney;System.out.println(account.name+"余额为"+account.money);System.out.println(this.getName()+"手里的钱:"+nowMoney);}
运行结果:
使用同步块锁账户
public void run() {synchronized (account){//判断卡里钱还够不够if (account.money-drawingMoney<0){System.out.println(Thread.currentThread().getName()+"钱不够了,取不了");return;}//sleep可以放大问题的发生性try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//卡里余额 = 余额 - 取的钱account.money = account.money-drawingMoney;//手里的钱nowMoney = drawingMoney+nowMoney;System.out.println(account.name+"余额为"+account.money);System.out.println(this.getName()+"手里的钱:"+nowMoney);}}
运行结果:
6. 总结
1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。
六、死锁
1. 概念
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
package com.wedu.TreadDemo;public class DeadThread{public static void main(String[] args) {WomanOfSubstance w1 = new WomanOfSubstance(0,"林黛玉");WomanOfSubstance w2 = new WomanOfSubstance(1,"王熙凤");w1.start();w2.start();}}class House{}class Car{}class WomanOfSubstance extends Thread{//需要的资源只有一份,通过static来保证只有一份static House house = new House();static Car car = new Car();int choice;//选择String girlName;//物质的女人WomanOfSubstance(int choice, String girlName){this.choice = choice;this.girlName = girlName;}@Overridepublic void run() {//选择try {choiceWhat();} catch (InterruptedException e) {e.printStackTrace();}}//选择,互相持有对方的锁,就是需要拿到对方的资源private void choiceWhat() throws InterruptedException {if (choice==0){synchronized (house){//获得房子的锁System.out.println(this.getName()+"获得房子的锁");Thread.sleep(1000);synchronized (car){//一秒钟后获得车子的锁System.out.println(this.getName()+"获得车子的锁");}}}else {synchronized (car){//获得房子的锁System.out.println(this.getName()+"获得房子的锁");Thread.sleep(2000);synchronized (house){//一秒钟后获得车子的锁System.out.println(this.getName()+"获得车子的锁");}}}}}
运行结果:程序卡住,无法结束
修改代码为下面这样,避免锁的获取嵌套即可:
//选择,互相持有对方的锁,就是需要拿到对方的资源private void choiceWhat() throws InterruptedException {if (choice==0){synchronized (house){//获得房子的锁System.out.println(this.getName()+"获得房子的锁");Thread.sleep(1000);}synchronized (car){//一秒钟后获得车子的锁System.out.println(this.getName()+"获得车子的锁");}}else {synchronized (car){//获得房子的锁System.out.println(this.getName()+"获得房子的锁");Thread.sleep(2000);}synchronized (house){//一秒钟后获得车子的锁System.out.println(this.getName()+"获得车子的锁");}}}
运行结果:
2. 死锁避免的条件
产生死锁的四个必要条件:
互斥条件:一个资源每次只能被─个进程使用。请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。
3. Lock锁
从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock(可重入锁) 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。package com.wedu.TreadDemo.syn;import java.util.concurrent.locks.ReentrantLock;public class LockTest {public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(ticket, "1,").start();new Thread(ticket, "2,").start();new Thread(ticket, "3,").start();}}class Ticket implements Runnable {int ticketNums = 10;//lock锁private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true) {try {lock.lock();//加锁后,使得线程安全if (ticketNums > 0) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+ticketNums--);} else {break;}} finally {//解锁lock.unlock();}}}}
运行结果:
4. synchronized与Lock的对比
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
七、生产者与消费者
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费。对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。在生产者消费者问题中,仅有synchronized是不够的。 synchronized可阻止并发更新同一个共享资源,实现了同步synchronized不能用来实现不同线程之间的消息传递(通信)
1. 线程通信
Java提供了几个方法解决线程之间的通信问题:
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常llegalMonitorStateException
2. 解决方式1——管程法
并发协作模型“生产者/消费者模式”—>管程法
生产者:负责生产数据的模块(可能是方法,对象,线程,进程)消费者:负责处理数据的模块(可能是方法,对象,线程,进程)缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
package com.wedu.TreadDemo.PC;//测试多线程生产者与消费者问题,解决方法——管程法(利用缓冲区)public class TestPC1 {public static void main(String[] args) {SynContainer synContainer = new SynContainer();new Producers(synContainer).start();new Consumers(synContainer).start();}}//生产者class Producers extends Thread{SynContainer synContainer;public Producers(SynContainer synContainer){this.synContainer = synContainer;}//生产@Overridepublic void run() {int i = 1;while (i<=100){System.out.println("生产了"+i+"只鸡");synContainer.push(new IKun(i));i++;}}}//消费者class Consumers extends Thread{SynContainer synContainer;public Consumers(SynContainer synContainer){this.synContainer = synContainer;}//消费@Overridepublic void run() {int i = 1;while (i<=100){System.out.println("消费了"+synContainer.pop().id+"只鸡");i++;}}}//产品class IKun{//第几个ikunint id;public IKun(int id) {this.id = id;}}//缓冲区class SynContainer{//1.定义一个容器IKun[] iKuns = new IKun[10];//容器计数器int count = 0;//生产者放入产品public synchronized void push(IKun iKun) {if (count == 10) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}iKuns[count] = iKun;count++;this.notifyAll();//唤醒消费者消费}//消费者消费产品public synchronized IKun pop() {if (count == 0) {try {this.wait();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}count--;IKun iKun = iKuns[count];this.notifyAll();//唤醒生产者生产return iKun;}}
运行结果:
3. 解决方式2——信号灯法
package com.wedu.TreadDemo.PC;//测试多线程生产者与消费者问题,解决方法——管程法(利用缓冲区)public class TestPC2 {public static void main(String[] args) {Food food = new Food();new Producer(food).start();new Consumer(food).start();}}//生产者class Producer extends Thread {Food food;public Producer(Food food) {this.food = food;}//生产@Overridepublic void run() {for (int i = 0; i < 20; i++) {if (i%2 == 0){this.food.make("老婆饼");}else {this.food.make("西北风");}}}}//消费者class Consumer extends Thread {Food food;public Consumer(Food food) {this.food = food;}//消费@Overridepublic void run() {for (int i = 0; i < 20; i++) {this.food.eat();}}}/*** 食物* 生产者生产食物的时候,消费者等待* 消费者消费食物的时候,生产者等待*/class Food {String foods;//生产的食物boolean flag = true;//旗帜//生产public synchronized void make(String foods){//生产等待if(!flag){//false就消费try {wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("生产者生产了:"+foods);//通知消费者消费this.notifyAll();this.foods = foods;this.flag = !flag;}//消费public synchronized void eat(){//消费等待if(flag){//true就生产try {wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("消费者消费了:"+foods);//通知消费者消费this.notifyAll();this.flag = !flag;}}
运行结果:
八、线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通。好处: 提高响应速度(减少了创建新线程的时间)降低资源消耗(重复利用线程池中线程,不需要每次都创建)便于线程管理(…) corePoolSize:核心池的大小maximumPoolSize:最大线程数keepAliveTime:线程没有任务时最多保持多长时间后会终止1. 使用线程池
JDK 5.0起提供了线程池相关API:ExecutorService 和 ExecutorsExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable Future submit(Callable task):执行任务,有返回值,一般用来执行Callablevoid shutdown():关闭连接池 Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池package com.wedu.TreadDemo.syn;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.ThreadPoolExecutor;//测试线程池public class TestPool {public static void main(String[] args) {ThreadPool threadPool = new ThreadPool();//1.创建线程池ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(10);//2.执行线程threadPoolExecutor.execute(threadPool);threadPoolExecutor.execute(threadPool);threadPoolExecutor.execute(threadPool);threadPoolExecutor.execute(threadPool);//关闭连接池threadPoolExecutor.shutdown();}}class ThreadPool implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"被创建了");}}
运行结果:
九、面试题拓展
1. 问:常听人说,在32位的机器上对long型变量进行加减操作存在并发隐患,到底是不是这样呢?
答:在32位的机器上对long型变量进行加减操作存在并发隐患的说法是正确的。
原因就是:线程切换带来的原子性问题。
非volatile类型的long和double型变量是8字节64位的,32位机器读或写这个变量时得把人家咔嚓分成两个32位操作,可能一个线程读了某个值的高32位,低32位已经被另一个线程改了。所以官方推荐最好把long、double 变量声明为volatile或是同步加锁synchronize以避免并发问题。
2.JUC并发包的理解
3. lock锁的创建
class Foo implements Runnable{private byte[] lock = new byte[0]; // 特殊的instance变量Public void methodA(){synchronized(lock) {//… }}//…..}
注:零长度的byte数组对象创建起来将比任何对象都经济,查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。