引言

如何理解多线程呢,目前我们写的程序都单线程,单线程指的是整个程序只有一条唯一执行路径 所有代码被串联到这条路径中。多线程指的就是一个程序中,存在类似多条执行路径。

一、多线程基础

程序、进程、线程

程序

  • 是为完成特定任务,用某种语言编写的一组指令的集合,是一段静态代码

进程

  • 是程序的一次执行过程,正在运行的一个程序。进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。

线程

  • 线程是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程gc线程
  • main()称之为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不是人为可干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销
  • 每个现场在自己的工作内存交互,内存控制不当会造成数据不一样

二、线程创建[高频面试]

继承Thread类

将类声明为**Thread**的子类。该子类**重写**Thread类的**run**方法。创建对象,开启线程。

Thread类的一些方法:
public final String getName() 获取线程名称
public final void setName(String name) 指定线程名称
public static Thread currentThread() 获取当前线程对象

开启线程的步骤:
1、定义一个**Thread类的子类****重写run方法**
2、创建自定义的线程子类对象
3、开启线程动作
**public void start() **使该线程开始执行

自动定义线程类

  1. //线程类
  2. class MyThread extends Thread{
  3. }

重写run()

  1. //线程类
  2. public class MyThread extends Thread {
  3. //重写run , 这个线程要做的事情
  4. @Override
  5. public void run(){
  6. for (int i = 1; i < 10; i++) {
  7. System.out.println("MyThread:"+i);
  8. }
  9. }
  10. }

创建并启动线程

  1. public class ThreadDemo {
  2. public static void main(String[] args) {
  3. //创建线程对象
  4. MyThread myThread = new MyThread();
  5. //启动线程
  6. myThread.start();
  7. //返回main线程
  8. Thread mainThread = Thread.currentThread();
  9. for (int i = 1; i < 10; i++) {
  10. System.out.println(mainThread.getName()+":"+i);
  11. }
  12. }
  13. }

启动线程 必须使用 **start() **,不可直接调用run(), 区别在于 调用start() 是触发JVM调用run() 为当前对象对象分配自己的栈空间,形成一条线程。 直接调用run() 则不会,就跟平时一样相当于掉一个普通方法

习题:购买火车票

  1. public class BuyTicketThread extends Thread{
  2. //设置线程名字
  3. public BuyTicketThread(String name){
  4. super(name);
  5. }
  6. //多个对象共享10张票
  7. static int ticketNum = 10;
  8. @Override
  9. public void run() {
  10. //每个窗口有100个人在排队
  11. for (int i = 1; i < 100; i++) {
  12. if(ticketNum > 0){ //判断,票数大于0开始抢票
  13. System.out.println("我在"+this.getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
  14. }
  15. }
  16. }
  17. }
  1. public class Test {
  2. public static void main(String[] args) {
  3. //多个窗口抢票:三个窗口三个线程对象
  4. BuyTicketThread t1 = new BuyTicketThread("窗口1");
  5. t1.start();
  6. BuyTicketThread t2 = new BuyTicketThread("窗口2");
  7. t2.start();
  8. BuyTicketThread t3 = new BuyTicketThread("窗口3");
  9. t3.start();
  10. }
  11. }

实现Runnable接口

Thread类本身就是Runnable的实现类,它还定义了一些管理线程的方法。
使用Runnable方式创建线程,依然要依赖Thread。Runnable 仅仅表达的是线程要执行的任务。使用这种方式的思想,希望把线程对象 与线程要执行的任务分离开来。

开启线程的步骤:
1、定义Runnable线程执行目标实现类,重写**run**方法
2、通过指定线程执行目标的构造方法创建线程对象
a) 创建线程执行目标对象
b) 通过线程执行目标创建线程对象
3、开启线程动作 (start开启线程)

创建Runnable实现类,重写**run**方法

  1. public class MyRunnable implements Runnable{
  2. @Override
  3. public void run() {
  4. //返回当前线程
  5. Thread thread = Thread.currentThread();
  6. for (int i = 1; i <10 ; i++) {
  7. System.out.println(thread.getName()+":"+i);
  8. }
  9. }
  10. }

创建线程并启动(通过指定线程执行目标的构造方法创建线程对象并开启线程)

  1. public class RunnableDemo {
  2. public static void main(String[] args) {
  3. //创建线程执行目标
  4. MyRunnable mr = new MyRunnable();
  5. //通过指定线程执行目标的构造方法创建线程对象
  6. Thread thread = new Thread(mr);
  7. thread.setName("Hi");
  8. //开启线程动作
  9. thread.start();
  10. //返回main线程
  11. Thread mainThread = Thread.currentThread();
  12. for (int i = 1; i < 10; i++) {
  13. System.out.println(mainThread.getName()+":"+i);
  14. }
  15. }
  16. }

习题:买火车票

  1. public class BuyTicketThread implements Runnable{
  2. //多个对象共享10张票
  3. int ticketNum = 10;
  4. @Override
  5. public void run() {
  6. for (int i = 1; i < 100; i++) {
  7. if(ticketNum > 0){
  8. System.out.println("我在"+Thread.currentThread().getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
  9. }
  10. }
  11. }
  12. }
  1. public class Test {
  2. public static void main(String[] args) {
  3. //定义线程对象
  4. BuyTicketThread t = new BuyTicketThread();
  5. //窗口1买票
  6. Thread t1 = new Thread(t, "窗口1");
  7. t1.start();
  8. //窗口2买票
  9. Thread t2 = new Thread(t, "窗口2");
  10. t2.start();
  11. //窗口1买票
  12. Thread t3 = new Thread(t, "窗口3");
  13. t3.start();
  14. }
  15. }

两种实现方式对比

方式一,线程对象和线程任务是耦合的,方式二,线程对象和线程任务是分离的
方法一,扩展性不好,类单继承的。 方式二,扩展性更好,接口是多实现。共享资源能力强,不需要加**static**修饰
方法一,调用线程方法直接 ,方式二,不可直接调用线程方法,需要先调用 **Thread._currentThread_()**,这个方法的作用是拿到与当前线程任务绑定的线程对象。

实现Callable接口

  • 是在JDK1.5之后出现的。

优点:①有返回值 ②能抛出异常
缺点:创建线程比较麻烦

  1. public class TestRandomNum implements Callable<Integer> {
  2. /*
  3. 1、实现Callable接口,可以不带泛型,如果不带泛型,那么call方式的返回值是Object类型
  4. 2、如果带泛型,那么call的返回值就是泛型对应的类型
  5. 3、从call方法看到,方法有返回值,可以抛出异常
  6. */
  7. @Override
  8. public Integer call() throws Exception {
  9. return new Random().nextInt(10);//返回10以内的随机数
  10. }
  11. }
  12. class Test{
  13. public static void main(String[] args) throws ExecutionException,InterruptedException {
  14. //定义一个线程对象
  15. TestRandomNum trn = new TestRandomNum();
  16. FutureTask ft = new FutureTask(trn);
  17. Thread t = new Thread(ft);
  18. t.start();
  19. //获取线程得到的返回值
  20. Object o = ft.get();
  21. System.out.println(o);
  22. }
  23. }

三、线程的生命周期

线程的生命周期包括**5**个阶段,**新建****就绪****运行****阻塞****销毁**

  • 新建:使用**new**关键字创建一个线程对象,这个阶段仅仅是在**JVM**中开辟一个内存空间;
  • 就绪:调用线程对象的**.start方法()**,使线程处于**runnable**可运行状态;
  • 运行:**CPU**分配时间片给线程,线程开始执行run方法里面定义的各种操作;
  • 阻塞:线程在运行状态的时候,可能会遇到一些特殊情况,导致线程停止下来,如**sleep()**,**wait()**,处于阻塞状态的线程需要等待其他机制使得线程的阻塞状态被唤醒,比如调用**notify()**,**notifyall()**。被唤醒的线程不会立即进行执行run方法的操作,而是需要等待CPU重新分配时间片进行执行;
  • 销毁:线程执行完毕,或者线程被终止、或者线程里面执行的方法出现异常,就会导致线程被销毁,释放资源;

线程生命周期图
第16章:多线程 - 图1
线程生命周期状态流转详解

我们字节码执行引擎执行一下代码

  1. Thead thread = new Thread();

这时间线程的创建,属于JVM中新增一个对象,给对象分配内存空间,并不是指操作系统创建了一个线程,接着上面的代码,我们调用thread对象的**start()**方法时候,线程才会被创建出来,进入就绪runnable状态,这是一个可运行状态,等待CPU分配时间片。

  1. thread.start();

当线程处于Runnable状态后,会有什么样的变化呢?
线程处于Runnable顾名思义,线程此时处于一种可以被运行状态,当分配到CPU时间片后,那么就会进入运行状态
反过来思考,Runnable状态可否进入Blocked状态或者Terminated状态吗?
这明显是不行的,Runnable只是一种可运行状态,只有真正运行中的线程,才可以被阻塞或者被终止,一个线程都没有真正被执行,怎么会进入被阻塞或者终止呢?
当线程分配到时间片后,进入运行Running状态;

当线程处于Running状态后,会有什么变化呢?
正在运行的线程,如果顺利执行完任务之后,那么任务结束,线程就应该被销毁,面得占用CPU资源,因此很容易想到线程会到达Terminated状态
正在运行的线程,如果中途需要停止下来那也是可以的,这个时候线程就会被阻塞起来,一直停留在某个阶段
正在运行的线程,如果运行中,出现系统异常,这样就会导致线程直接进入Terminated状态
正在运行的线程,我们也可以将其直接返回就绪状态,比如调用yield方法

当线程处于Blocked状态后会出现什么情况呢?
被阻塞的线程,自然会有被唤醒时,但是被唤醒的线程,不能直接进入Running状态,而是先进入Runnable状态,等待CPU重新分配时间片
可以思考一下,如果Blocked中的线程直接进入Running状态吗?不可能的,因为线程时靠CPU分配时间片来运行的,CPU没有分配时间片,线程是没有权限跑起来的,因此,必须回到Runnable状态,然后等待CPU分配时间片。
同时,在阻塞状态的线程也可以通过interrupted 方法进行中断

使线程处于阻塞状态的方法有sleep(),wait(),这两者有什么区别呢?
使用sleep()方法,使线程处于睡眠状态,等待指定时间就会运行,如果代码块被加了锁,则线程不释放锁;
使用wait()方法,使线程处于阻塞状态的时候,wait()方法自动释放了锁,而且wait()方法必须使用notify()或者notifyAll()方法才能唤醒线程
sleep(1000)代表在阻塞1s,1s内部释放锁,也不会分配到CPU时间片,过了1s后进入runnable状态,重新等待执行时间片
wait(1000),占用锁1s中,过后1s后释放锁,但是还是出于阻塞状态,直到notify()/notifyAll()

四、线程常见方法

|

start()

启动线程
**run()** 线程类,继承**Thread**类或者实现**Runnable**接口的时候,都需要实现这个run()方法,run()方法里面是线程要执行的内容
currentThread **Thread**类中一个静态方法,获取当前正在执行的线程
setName()/getName() 取名字/获得名字,如果没有设置名字,默认名字为Thread-(0-n)
setPriority(int xx)/getPriority() 设置/获取优先级 ,默认三个 MIN_PRIORITY = 1;NORM_PRIORITY = 5;MAX_PRIORITY = 10;
join() 同步,加入线程线程中 注意:必须先start,再join才有效
Thread.sleep( long ms ) 睡眠 暂停执行,直到睡眠时间到
setDeamon(boolean xx)/isDeamon() 设置伴随进程:将子线程设置为主线程的伴随进程,主线程停止的时候,子线程也不要继续执行了(先设置,再启动)
Thread.yield() 线程礼让. 让出cpu执行时间片,自己进入就绪状态,再次等待调度
stop() 停止线程,过期不建议使用

五、线程安全问题[重点]

什么时候会出现线程安全问题

单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会导致共享的资源出现问题,一个线程还没执行完,另一个线程就参与进来了,开始争抢

购买火车票出现问题:**重票,错票**

  • 出现2个或者3个10张票

    1. 线程1: 我在窗口1买到了广州到北京的第10张火车票,还没等`--`操作,就被线程2抢走了资源<br /> 线程2: 我在窗口2买到了广州到北京的第10张火车票,还没等`--`操作,就被线程3抢走了资源<br /> 线程3: 我在窗口2买到了广州到北京的第10张火车票,这个时候执行了`--`操作,则票数为`**9**`
  • 出现0,-1,-2张票

    1. 线程1: 我在窗口1买到了广州到北京的第1张火车票,还没等`--`操作,就被线程2抢走了资源<br /> 线程2: 我在窗口2买到了广州到北京的第1张火车票,就被线程3抢走了资源<br /> 线程3: 我在窗口2买到了广州到北京的第1张火车票,就被线程1抢走了资源<br /> 线程1: 执行`--`操作,票数为`**0**`<br /> 线程2: 执行`--`操作,票数为`**-1**`

如何解决线程安全问题

方法1: 同步代码块

  • Java中使用**synchronized(obj)**关键字来解决,将一个完整动作使用synchronized包裹。原理当线程执行到有synchronized的代码块时,先尝试去获取obj 对象的锁(每个对象有且只有一把),如果取到了则进入代码块中执行,期间不被打扰(如果这时有其他线程准备进入代码块但是取不到锁),直到执行完毕后,自动归还锁。其他线程方可进入。

  • 同步代码块中的锁对象可以是任意的对象;但多个线程操作相同数据时,要使用同一个锁对象才能够保证线程安全,避免安全问题

    1. synchronized (锁对象) {
    2. 可能会产生线程安全问题的代码
    3. }

同步代码块演示1:

synchronized (this)this指调用当前对象,相当于调用一个t对象。

  1. //窗口线程
  2. public class BuyTicketThread implements Runnable{
  3. //多个对象共享10张票
  4. int ticketNum = 10;
  5. @Override
  6. public void run() {
  7. for (int i = 1; i < 100; i++) {
  8. synchronized (this) {//把具有安全隐患的代码锁住即可
  9. if (ticketNum > 0) {
  10. System.out.println("我在" + Thread.currentThread().getName() + "买到了广州到北京的第" + ticketNum-- + "张车票");
  11. }
  12. }
  13. }
  14. }}
  1. //同步案例
  2. public class Test01 {
  3. public static void main(String[] args) {
  4. //定义线程对象
  5. BuyTicketThread t = new BuyTicketThread();
  6. //窗口1买票
  7. Thread t1 = new Thread(t, "窗口1");
  8. t1.start();
  9. //窗口2买票
  10. Thread t2 = new Thread(t, "窗口1");
  11. t2.start();
  12. //窗口1买票
  13. Thread t3 = new Thread(t, "窗口1");
  14. t3.start();
  15. }
  16. }

同步代码块演示2:

synchronized(**BuyTicketThread.class**)小括号**()**里需要**一个对象**,这个对对象必须满足,不同线程进入时是同一个对象。这样才有互斥性(多个线程用同一把锁)

  1. public class BuyTicketThread extends Thread {
  2. //设置线程名字
  3. public BuyTicketThread(String name){
  4. super(name);
  5. }
  6. //多个对象共享10张票
  7. static int ticketNum = 10;
  8. @Override
  9. public void run() {
  10. //每个窗口有100个人在排队
  11. for (int i = 1; i < 100; i++) {
  12. synchronized (BuyTicketThread.class) {//多个线程用同一把锁
  13. if (ticketNum > 0) { //判断,票数大于0开始抢票
  14. System.out.println("我在" + this.getName() + "买到了广州到北京的第" + ticketNum-- + "张车票");
  15. }
  16. }
  17. }
  18. }}
  1. public class Test {
  2. public static void main(String[] args) {
  3. //多个窗口抢票:三个窗口三个线程对象
  4. BuyTicketThread t1 = new BuyTicketThread("窗口1");
  5. t1.start();
  6. BuyTicketThread t2 = new BuyTicketThread("窗口2");
  7. t2.start();
  8. BuyTicketThread t3 = new BuyTicketThread("窗口3");
  9. t3.start();
  10. }
  11. }

同步监视器总结

总结1:认识同步监视器(锁子)——>synchronized(同步监视器)

  • 必须是**引用数据类型**,不能是基本数据类型
  • 可创建一个专门的同步监视器,没有任何业务含义
  • 一般使用共享资源做同步监视器即可
  • 在同步代码块中不能改变同步监视器对象的引用
  • 尽量不要使用String和包装类Integer做同步监视器
  • 建议使用final修饰同步监视器

总结2:同步代码块的执行过程

  • 第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
  • 第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
  • 第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
  • 第一个线程再次获取cpu,接着执行后续代码,同步代码块执行完毕,释放锁open
  • 第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)

    1. 强调:同步代码块中能发生`**cpu**`的切换吗,能!但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧`close`

总结3:其他

  • 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其他的任何一个代码块
  • 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块

方法2: 同步方法

同步方法演示1:

  1. public class BuyTicketThread implements Runnable{
  2. //多个对象共享10张票
  3. int ticketNum = 10;
  4. @Override
  5. public void run() {
  6. for (int i = 1; i < 100; i++) {
  7. buyTicket();
  8. }
  9. }
  10. public synchronized void buyTicket(){//把具有安全隐患的代码锁住即可
  11. if(ticketNum > 0){
  12. System.out.println("我在"+Thread.currentThread().getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
  13. }
  14. }
  15. }
  1. public class Test {
  2. public static void main(String[] args) {
  3. //定义线程对象
  4. BuyTicketThread t = new BuyTicketThread();
  5. //窗口1买票
  6. Thread t1 = new Thread(t, "窗口1");
  7. t1.start();
  8. //窗口2买票
  9. Thread t2 = new Thread(t, "窗口2");
  10. t2.start();
  11. //窗口1买票
  12. Thread t3 = new Thread(t, "窗口3");
  13. t3.start();
  14. }
  15. }

同步方法演示1:

  1. public class BuyTicketThread extends Thread{
  2. //设置线程名字
  3. public BuyTicketThread(String name){
  4. super(name);
  5. }
  6. //多个对象共享10张票
  7. static int ticketNum = 10;
  8. @Override
  9. public void run() {
  10. //每个窗口有100个人在排队
  11. for (int i = 1; i < 100; i++) {
  12. buyTicket();
  13. }
  14. }
  15. public static synchronized void buyTicket(){
  16. if(ticketNum > 0){ //判断,票数大于0开始抢票
  17. System.out.println("我在"+Thread.currentThread().getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
  18. }
  19. }
  20. }
  1. public class Test {
  2. public static void main(String[] args) {
  3. //多个窗口抢票:三个窗口三个线程对象
  4. BuyTicketThread t1 = new BuyTicketThread("窗口1");
  5. t1.start();
  6. BuyTicketThread t2 = new BuyTicketThread("窗口2");
  7. t2.start();
  8. BuyTicketThread t3 = new BuyTicketThread("窗口3");
  9. t3.start();
  10. }
  11. }

总结:

  • 不要将run()定义为同步方法,效率太低
  • 非静态同步方法的同步监视器是this,静态同步方法的同步监视器是类名.class字节码信息对象
  • 同步代码块的效率要高于同步方法(原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部)
  • 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法,同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块

方法3: Lock锁

  • JDK1.5后新增的
  • 与采用synchronized相比,Lock可提供多种锁方案,更灵活
  • synchronized是java中的关键字,是靠jvm来识别完成的,是虚拟机级别的
  • Lock锁上API级别的,提供了相应的借口和对应的实现类,更灵活,表现出来的性能优于之前的方式

代码演示

  1. public class BuyTicketThread extends Thread{
  2. //设置线程名字
  3. public BuyTicketThread(String name){
  4. super(name);
  5. }
  6. //多个对象共享10张票
  7. static int ticketNum = 10;
  8. //拿来一把锁
  9. Lock lock =new ReentrantLock();
  10. @Override
  11. public void run() {
  12. //每个窗口有100个人在排队
  13. for (int i = 1; i < 100; i++) {
  14. //打开锁
  15. lock.lock();
  16. try {
  17. if(ticketNum > 0){ //判断,票数大于0开始抢票
  18. System.out.println("我在"+this.getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
  19. }
  20. }catch(Exception e){
  21. e.printStackTrace();
  22. }finally {
  23. //关闭锁:即使有异常,这个锁也可以得到释放
  24. lock.unlock();
  25. }
  26. }
  27. }}

Lock和synchronized的区别

  • Lock显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized隐式锁
  • Lock只有代码块锁synchronized代码块锁和方法锁
  • 使用Lock锁,JVM将话费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)

�优先使用顺序
Lock—->同步代码块(已经进入了方法体,分配了相应资源)—->同步方法(在方法体之外)

死锁问题[高频面试]

  • 多个线程因为竞争资源而陷入相互等待的情况,无法恢复的场景。

产生死锁的必要条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程—资源的环形链。

  1. public class TestDeadLock implements Runnable {
  2. public int flag = 1;
  3. static Object o1 = new Object(),o2 = new Object();
  4. @Override
  5. public void run() {
  6. System.out.println("flag=" + flag);
  7. //当flag = 1锁住o1
  8. if (flag == 1) {
  9. synchronized (o1) {
  10. try {
  11. Thread.sleep(200);
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. }
  15. //只要锁住o2就完成
  16. synchronized (o2) {
  17. System.out.println("2");
  18. }
  19. }
  20. }
  21. //如果flag==0锁住o2
  22. if (flag == 0) {
  23. synchronized (o2) {
  24. try {
  25. Thread.sleep(200);
  26. } catch (Exception e) {
  27. e.printStackTrace();
  28. }
  29. //只要锁住o2就完成
  30. synchronized (o1) {
  31. System.out.println("3");
  32. }
  33. }
  34. }
  35. }
  36. public static void main(String[] args) {
  37. //实例2个线程累
  38. TestDeadLock td1 = new TestDeadLock();
  39. TestDeadLock td2 = new TestDeadLock();
  40. td1.flag = 1;
  41. td2.flag = 0;
  42. //开启2个线程
  43. Thread t1 = new Thread(td1);
  44. Thread t2 = new Thread(td2);
  45. t1.start();
  46. t2.start();
  47. }
  48. }

预防死锁:
资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
解决方法:减少同步资源的定义,避免嵌套同步

六、线程通信问题

什么是线程通信

协调多个线程有序访问某些资源。

原理

在java对象中,有两种池
锁池:synchronized
等待池:wait()notify()notifyAll()
如果一个线程调用了某个对象的wait()方法,那么该线程进入到该对象的等待池中(并且已经将锁释放)
如果未来的某一时刻,另外一个现场调用了相同对象的notify方法或者notifyAll方法,那么该等待池中的线程就会被唤起,然后进入到对象的锁池里面去获得该对象的锁,如果获得锁成功,那么该线程就会向wait方法之后的路径继续执行,注意是向wait方法之后

线程通信方法

wait(): 它是Object的方法,不是线程提供的方法,让执行这个wait()调用的线程等待。 无限期等待,直到有被唤醒。
wait( long ms ) : 等待指定的毫米数,如果没有被唤醒,则自动唤醒
wait( long ms, int naos) : 更精确的等待时间
notify(): 唤醒等待在该对象上的线程(只会唤醒一个)。
notifyAll(): 唤醒全部等待在该对象上的全部线程。

这些方法必须使用在存在 synchronized 代码块内 或者 synchronized 方法中。

  1. package waitandnofity;
  2. /**
  3. * 等待唤醒案例
  4. */
  5. public class WaitAndNotifyCase {
  6. public static void main(String[] args) throws InterruptedException {
  7. Object obj = new Object();
  8. Thread son1 = new Thread( new Foo(obj) );
  9. son1.setName("张三");
  10. Thread son2 = new Thread( new Foo(obj) );
  11. son2.setName("李四");
  12. son1.start();
  13. son2.start();
  14. //让主线程等5s后去唤醒其他线程
  15. Thread.sleep(5000);
  16. synchronized (obj){
  17. //obj.notify();//唤醒一个
  18. obj.notifyAll();//唤醒全部
  19. }
  20. }
  21. }
  22. class Foo implements Runnable{
  23. Object obj = null;
  24. public Foo(Object obj) {
  25. this.obj = obj;
  26. }
  27. @Override
  28. public void run() {
  29. synchronized (obj){
  30. System.out.println(Thread.currentThread().getName()+ "执行开始...");
  31. try {
  32. obj.wait();
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. System.out.println(Thread.currentThread().getName()+ "执行完毕...");
  37. }
  38. }
  39. }

生产者消费者模型

  1. package producerconsumer;
  2. /**
  3. * 生产者和消费者案例
  4. */
  5. public class ProducerAndConsumer {
  6. public static void main(String[] args) {
  7. //容器
  8. Container container = new Container();
  9. Thread xfz1 = new Thread( new Consumer(container) );
  10. xfz1.setName("张三");
  11. xfz1.start();
  12. Thread xfz2 = new Thread( new Consumer(container) );
  13. xfz2.setName("李四");
  14. xfz2.start();
  15. Thread scz1 = new Thread( new Producer( container ) );
  16. scz1.setName("王师傅");
  17. scz1.start();
  18. Thread scz2 = new Thread( new Producer( container ) );
  19. scz2.setName("李师傅");
  20. scz2.start();
  21. }
  22. }
  23. //抽象出一个容器事物
  24. class Container{
  25. Object[] data = new Object[10]; //底层使用数组模拟栈
  26. int size; //元素计数器
  27. //存
  28. public synchronized void add( Object obj ){
  29. while( size==data.length ){ //存满了,不能存了,生产者线程,等待
  30. try {
  31. this.wait();
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. data[size++] = obj;
  37. System.out.println(Thread.currentThread().getName() + "生产"+obj);
  38. //唤醒刚刚因为没有商品而等待的消费者线程
  39. this.notifyAll();
  40. }
  41. //取
  42. public synchronized Object get(){
  43. while (size==0){
  44. try {
  45. this.wait(); //商品不足 消费者线程等待。
  46. } catch (InterruptedException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. Object result = data[--size];
  51. System.out.println(Thread.currentThread().getName() +"消费"+result);
  52. //唤醒刚刚因为没有空间 而等待的生产者线程
  53. this.notifyAll();
  54. return result;
  55. }
  56. }
  57. //生产者
  58. class Producer implements Runnable{
  59. //共享容器
  60. Container container ;
  61. public Producer(Container container) {
  62. this.container = container;
  63. }
  64. @Override
  65. public void run() {
  66. for ( int i =1;i<=20; i++ ){
  67. container.add( "汉堡"+i);
  68. }
  69. }
  70. }
  71. //消费者
  72. class Consumer implements Runnable{
  73. //共享容器
  74. Container container ;
  75. public Consumer(Container container) {
  76. this.container = container;
  77. }
  78. @Override
  79. public void run() {
  80. for ( int i =1;i<=20; i++ ){
  81. container.get();
  82. }
  83. }
  84. }

sleep和wait的区别

  • sleep进入阻塞状态没有释放锁,wait进入阻塞状态且同时释放锁

七.JUC锁[高频面试]

锁分类

锁的作用就是保证数据一致性,但是往往就会牺牲效率,所以使用锁需要自己做好平衡和取舍。同等情况下,当然使用性能更好的锁,是最好的方案。

悲观锁

怀疑任何情况都会出现并发问题,所以在设计的时候 就默认锁定,jdk1.5前就是 synchronized ,早期说这个性能有问题(经过优化现在一般不去比较了)。在JDK1.5新的解决方法是提供了Lock接口和他的实现类。例如 ReentrantLock ReentrantReadWriteLock等。

乐观锁

乐观锁就是和悲观锁相反的思想,就是先不怀疑存在并发问题,如果确有存在再解决,通常需要为并发修改的数据设定一个版本号,修改前对一下先前获取的版本号,修改时在比较版本号如果一致才修改,不一致说明有版本变化不可修改。CAS算法就是一种乐观锁算法,它是硬件层面实现的,把多语句指令和为一个指令,确保了原子操作。
Compare And Swap( 比较 和 交换 ) V (内存值) E(期望值) N(新值) 当且仅当 V==E时 才将 V=N 。

ReentrantLock

JDK1.5 引入全新的锁体系,JUC包大部分都是这个版本引入的。ReentrantLock是Lock接口的实现类,意为重入锁(所谓重入锁就是允许同一个线程多次对对象去锁),具有和synchronized ,还提供了一些方法。功能更强大

  1. package lock.synchronize.block;
  2. import java.util.concurrent.locks.Lock;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. //同步案例
  5. public class TicketsDemo {
  6. public static void main(String[] args) {
  7. //模拟3个窗口
  8. Thread win1 = new Thread( new Window() );
  9. win1.setName("张阿姨");
  10. win1.start();
  11. Thread win2 = new Thread( new Window() );
  12. win2.setName("王阿姨");
  13. win2.start();
  14. Thread win3 = new Thread( new Window() );
  15. win3.setName("李阿姨");
  16. win3.start();
  17. }
  18. }
  19. //窗口线程
  20. class Window implements Runnable{
  21. //声明锁
  22. static Lock lock = new ReentrantLock(true);
  23. //模拟100张票
  24. static int total = 100;
  25. @Override
  26. public void run() {
  27. while (true){
  28. try{
  29. lock.lock(); //获得锁
  30. if(total>0) {
  31. total--;
  32. System.out.println(Thread.currentThread().getName() + "卖出1张,还剩" + total);
  33. }else{
  34. break;
  35. }
  36. }finally {
  37. lock.unlock(); //确保一定可以解锁
  38. }
  39. }
  40. }
  41. }

和对比 synchronized

  1. synchronized 关键字 ReentrantLock 类(jdk1.5新引入)
  2. 都是重入锁(ReentrantLock 功能强大 提供了更多的方法,以及实现公平锁策略)
  3. synchronized 上锁与解锁自动完成 , ReentrantLock 需要自己上锁和解锁。

ReadWriteLock

凡是用锁都会影响效率,如果把这种影响做到最小了呢? 读写锁就是一种经过细化考量的一种优化的锁。这种锁的特点是 读-读不阻塞线程, 读-写要阻塞线程,写-写要阻塞。一个把读写锁可以分离出 读锁和写锁。

  1. package lock.readwriteLock;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.locks.ReadWriteLock;
  5. import java.util.concurrent.locks.ReentrantReadWriteLock;
  6. /**
  7. * 读写锁案例
  8. */
  9. public class TestReadWriteLockCase {
  10. public static void main(String[] args) {
  11. long begin = System.currentTimeMillis();
  12. //并发访问源
  13. Foo obj = new Foo();
  14. //线程池
  15. ExecutorService executorService = Executors.newFixedThreadPool(20);
  16. //提交任务 18 次读
  17. for(int i= 0; i<18; i++){
  18. executorService.submit(new Runnable() {
  19. @Override
  20. public void run() {
  21. obj.getAge();
  22. }
  23. });
  24. }
  25. //提交2次写任务
  26. for(int i=0;i<2;i++){
  27. executorService.submit(new Runnable() {
  28. @Override
  29. public void run() {
  30. obj.setAge( 100 );
  31. }
  32. });
  33. }
  34. //关闭
  35. executorService.shutdown();
  36. while ( !executorService.isTerminated() ){
  37. }
  38. long end = System.currentTimeMillis();
  39. //计算时间
  40. System.out.println( (end-begin)/1000 );
  41. }
  42. }
  43. class Foo {
  44. private Integer age ;
  45. private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  46. public Integer getAge() {
  47. try {
  48. readWriteLock.readLock().lock();
  49. Thread.sleep(1000);
  50. } catch (InterruptedException e) {
  51. e.printStackTrace();
  52. }finally {
  53. readWriteLock.readLock().unlock();
  54. }
  55. return age;
  56. }
  57. public void setAge(Integer age) {
  58. try {
  59. readWriteLock.writeLock().lock();
  60. Thread.sleep(1000);
  61. } catch (InterruptedException e) {
  62. e.printStackTrace();
  63. }finally {
  64. readWriteLock.writeLock().unlock();
  65. }
  66. this.age = age;
  67. }
  68. }

Condition 通信

在jdk1.5以后,如果不使用 synchronized 同步方法, 那么线程通信可以使用Condition 实现,它的核心做法是,

  1. 获得一个 阻塞条件对象 Conditon cd = lock.newCondition();
  2. 阻塞方法 cd.awiat() 作用同 wait()
  3. 唤醒方法 cd.signal() / cd.signalAll() 作用同 notify() 和 notifyAll()
    ```java package lock.producerconsumer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /**

    • 生产者和消费者案例 */ public class ProducerAndConsumer { public static void main(String[] args) { //容器 Container container = new Container(); Thread xfz1 = new Thread( new Consumer(container) ); xfz1.setName(“张三”); xfz1.start();

      Thread scz1 = new Thread( new Producer( container ) ); scz1.setName(“王师傅”); scz1.start(); } }

//抽象出一个容器事物 class Container{ private Object[] data = new Object[10]; //底层使用数组模拟栈 private int size; //元素计数器 private ReentrantLock lock = new ReentrantLock(); //从锁上获取阻塞条件 private Condition cd = lock.newCondition(); //存 public void add( Object obj ){

  1. try{
  2. lock.lock();
  3. while( size==data.length ){ //存满了,不能存了,生产者线程,等待
  4. try {
  5. cd.await();
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. data[size++] = obj;
  11. System.out.println(Thread.currentThread().getName() + "生产"+obj);
  12. //唤醒刚刚因为没有商品而等待的消费者线程
  13. //TODO
  14. cd.signalAll();
  15. }finally {
  16. lock.unlock();
  17. }
  18. }
  19. //取
  20. public Object get(){
  21. try{
  22. lock.lock();
  23. while (size==0){
  24. try {
  25. cd.await(); ; //商品不足 消费者线程等待。
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. Object result = data[--size];
  31. System.out.println(Thread.currentThread().getName() +"消费"+result);
  32. //唤醒刚刚因为没有空间 而等待的生产者线程
  33. cd.signalAll();
  34. return result;
  35. }finally {
  36. lock.unlock();
  37. }
  38. }

} //生产者 class Producer implements Runnable{ //共享容器 Container container ;

  1. public Producer(Container container) {
  2. this.container = container;
  3. }
  4. @Override
  5. public void run() {
  6. for ( int i =1;i<=20; i++ ){
  7. container.add( "汉堡"+i);
  8. }
  9. }

} //消费者 class Consumer implements Runnable{ //共享容器 Container container ; public Consumer(Container container) { this.container = container; } @Override public void run() { for ( int i =1;i<=20; i++ ){ container.get(); } } } ```