多线程编程

了解多线程编程先了解进程,因为线程依赖于进程。

进程和线程

所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行状态的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

进程的三个特征

  1. 独立性:进程是系统中独立存在的实体,它可以拥有自己的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  2. 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而程序是一个正在系统中活动的指令集合。在进程中加入了时间概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中是不具备的。
  3. 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

并发性和并行性概念辨析

  • 并发性:指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
  • 并行性:指在同一时刻,有多条指令在多个处理器上同时执行。

多进程
大部分操作系统都支持多进程并发执行,比如就像java程序员一边用着编译器敲代码、一边在播放器放着音乐、一边在查看电子文档这看起来就像是同时执行的。
但事实上对于一个CPU而言,它在某个时间点只能执行一个程序,也就是只能运行一个进程。CPU不断在这些进程之间来换切换执行,只不过切换的速度非常快,用户感觉不到,给人感觉是在同时执行。

多进程的意义

可以使计算机同时做多个事情,提高了CPU的使用率。

什么是多线程?

线程(Thread)也被称为轻量级进程,线程是进程的执行单元,是程序使用CPU的基本单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化之后,主线程就被创建了,对于大多数应用来说,通常只需要一个主线程,也可以创建多条执行流,这些执行流就是线程。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享所拥有的全部资源。

线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说线程的执行是随机的,正在执行的线程随时有可能被挂起,而正在就绪的线程也随时被执行。

从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行,但操作系统无须将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配;可以说操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

多线程的优势

  1. 进程之间不能共享内存,但线程之间共享内存非常容易。
  2. 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程实现多任务并发比多进程效率高;
  3. Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

多线程的意义
多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。多个线程是在抢CPU的执行权限,哪个进程的执行路径多也就是包含的线程数量多抢到CPU执行权限的概率就越大。

多线程原理图

å¨è¿éæå¥å¾çæè¿°

图中白色的条状代表CPU执行的时间片。CPU的执行时间片在多个线程之间来回随机切换。

Java中的多线程

为什么Java只讨论多线程,而不讨论多进程?

进程是对操作系统而言的,而线程是对程序而言的,Java代表的是程序,每次运行一个Java程序就相当于一个进程。

线程的创建和启动

Java使用Thread类代表线程,所有线程对象都是Thread类或子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流。

继承Thread类创建线程类

步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法成为线程执行体;
  2. 创建Thread子类的实例,即创建线程对象;
  3. 调用线程对象的start方法来启动该线程;
public class FirstThread extends Thread{

        //重写run方法,run方法的方法体就是线程执行体
        @Override
        public void run() {
            for (int i=0;i<100;i++){
                //getName()获取当前线程的线程名,省略了this
                //也可以使用类方法Thread.currentThread().getName()来获取线程名
                System.out.println(getName()+"-->"+i);
//            System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }

        public static void main(String[] args) {
            //创建两个线程对象
            FirstThread th1 = new FirstThread();
            FirstThread th2 = new FirstThread();
            //给线程设置名字
            th1.setName("java");
            th2.setName("c++");
            //开启线程
            th1.start();
            th2.start();
        }
    }

执行结果:

Java>>>>0
c++>>>>0
Java>>>>1
c++>>>>1
Java>>>>2
c++>>>>2
Java>>>>3
c++>>>>3
c++>>>>4
Java>>>>4

可以看到两个线程来回切换执行,不过是随机的。

虽然上面程序只显示两个线程,但实际上程序有三个线程,程序显式创建的2个子线程和主线程。当java程序开始运行之后,程序至少会创建一个主线程。主线程的执行体不是run方法确定的,而是由main()方法确定的。

实现Runable接口创建线程类

实现Runnable接口来创建并启动多线程的步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

执行结果:

新线程1>>>0
新线程2>>>0
新线程2>>>1
新线程2>>>2
新线程1>>>1
新线程2>>>3
新线程1>>>2
新线程2>>>4
新线程1>>>3
新线程1>>>4

Runnable接口中只有一个抽象方法,所以它是函数式接口,可以使用Lambda表达式(下篇博客介绍)来创建Runnable对象。

public class LambdaThread {
	public static void main(String[] args) {
		new Thread(() ->{
			for (int i=0;i<5;i++){
                //getName()获取当前线程的线程名,省略了this
                //也可以使用类方法Thread.currentThread().getName()来获取线程名
//                System.out.println(getName()+"-->"+i);
                System.out.println(Thread.currentThread().getName()+"-->"+(i+1));
            }
		}).start();
			
	}
}

执行结果:

Thread-0-->1
Thread-0-->2
Thread-0-->3
Thread-0-->4
Thread-0-->5

使用Callable和Future创建对象

从Java5开始,Java提供了一个Callable接口,该接口提供了一个call方法作为线程执行提,该方法比run方法功能更强大。

  • call方法可以有返回值;
  • call方法可以声明抛出异常;

java5提供了Future接口来代表Callable接口里的call方法的返回值,并为Future接口提供一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,可作为Thread类的target。

有如下方法来控制它关联的Callable任务:

  • boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务;
  • V get():返回Callable任务里call方法的返回值,调用该方法将造成线程阻塞,必须等到子线程结束后才会得到返回值。
  • V get(long timeout,TimeUnit unit):返回Callable任务里call方法的返回值,该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常。
  • boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true;
  • boolean isDone():如果Callable任务已完成,则返回true。

创建启动有返回值线程的步骤:

  • 创建Callable接口的实现类,并实现call方法,该call方法将作为线程执行体,且该call方法有返回值,在创建Callable实现类的实例。从java8开始可以直接使用Lambda表达式创建callable对象;
  • 使用FutureTask类来包装Callable对象,该Future对象封装了该Callable对象的call方法的返回值;
  • 使用功FutureTask对象作为Thread对象的target创建并启动新线程;
  • 调用功FutureTask对象的get()方法来实现线程类,并启动该线程;
public class ThirdThread {
    public static void main(String[] args) {
        //创建Callable对象
        ThirdThread rt = new ThirdThread();
        //使用Lambda表达式创建Callable<Integer>对象
        FutureTask task = new FutureTask<Integer>((Callable<Integer>)()->{
            int i=0;
            for (;i<100;i++){
                System.out.println(Thread.currentThread().getName()+"的循环变量i的值"+i);
            }
            return i;
        });
        for (int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"循环变量i的值"+i);
            if(i==20){
                new Thread(task,"有返回值的线程").start();
            }
        }
        //获取线程返回值
        try{
            System.out.println("子线程的返回值"+task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

执行部分结果:

有返回值的线程的循环变量i的值9
main循环变量i的值37
main循环变量i的值38
main循环变量i的值39
有返回值的线程的循环变量i的值10
main循环变量i的值40
有返回值的线程的循环变量i的值11
main循环变量i的值41
main循环变量i的值42
有返回值的线程的循环变量i的值12
有返回值的线程的循环变量i的值13
有返回值的线程的循环变量i的值14
有返回值的线程的循环变量i的值15

创建线程的三种方式对比

Runnable和Callable方式基本类似,所以归为一种。

采用Runnable和Callable接口的方式创建的优缺点:

  • 线程类只是实现了接口,还可以继承其他类;
  • 在这种方式下多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,较好体先面向对象的思想。
  • 缺点是,编程比较复杂;

采用Thread类的方式创建多线程的优缺点:

  • 确定是,不能在继承别的类;
  • 优势,编写简单,可以直接使用this获得当前线程对象。

线程的生命周期

线程的生命周期中它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。

线程的状态:

  • 新建:新建的线程对象和其他java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值;
  • 就绪:当线程对象调用start()方法后,线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程表示可以运行了,只是在等待执行权限。取决于JVM里线程调度器的调度。
  • 运行:当就绪状态的线程获得了CPU,开始执行run方法的现成的执行体,则该线程处于运行状态。当一个线程开始运行之后,他不一定一直处于运行状态,对于采用抢占式策略的系统而言,系统会给每个可执行的线程一小时间段来处理任务。
  • 阻塞:被阻塞的线程就没有执行权限了,会在合适的时候重新进入就绪状态。
  • 死亡:线程被结束;

线程被阻塞的原因

  • 线程调用sleep()方法主动放弃所占用的处理器资源;
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
  • 线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有;
  • 线程正在等待某个通知(notify);
  • 程序调用了线程的suspend()方法将该线程挂起。

解除阻塞的方法:解除阻塞让线程重新进入就绪状态;

  • 调用sleep()方法的线程经过了指定时间;
  • 线程调用的阻塞式IO方法已经返回;
  • 线程成功地获得了试图取得的同步监视器;
  • 线程获得了正在等待的通知;
  • 出于挂起状态的线程被调用了resume恢复方法;

线程怎么死亡

  • run()或call()方法执行完成,线程正常结束;
  • 线程抛出一个未捕获的异常;
  • 调用stop()方法(容易造成死锁)。

                                                                    线程状态的转换图

å¨è¿éæå¥å¾çæè¿°

注意

  • 主线程结束时,其他线程不受任何影响,并不会随之结束,一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受主线程的影响。
  • 不要试图对一个死亡的线程调用start方法让它重新就绪。
  • 不要对新建的线程调用两个或多次start()方法。

控制线程

join线程

Thread提供了让一个线程等待另一个线程完成的方法——join方法,当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程执行完为止。

public class JoinThreadTest {
    static int i=0;

    public static void main(String[] args) throws InterruptedException {
        for (;i<100;i++){
            //获取主线程名字
            System.out.println(Thread.currentThread().getName()+"--->"+i);
            //当i=20时创建子线程
            if(i==20){
                //创建子线程
                Thread sonThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i1 = 0; i1 < 100; i1++) {
                            //获取子线程
                            System.out.println(Thread.currentThread().getName()+"--->"+i1);
                            //当i为30时,将子线程
                        }
                    }
                },"sonThread");
                sonThread.start();
                //将sonThread加入到join,直到子线程执行完,主线程才开始继续执行
                sonThread.join();
            }
        }
    }
}

执行结果:

main--->0
main--->1
main--->2
main--->3
main--->4
main--->5
sonThread--->0
sonThread--->1
sonThread--->2
sonThread--->3
sonThread--->4
main--->6
main--->7
main--->8
main--->9

当主线程循环到5时创建子线程,启动并且join到执行流,此时主线程开始阻塞,直到子线程执行完,主线程继续开始从6执行。

join方法的三种重载形式:

  • join():等待被join的线程执行完成;
  • join(long millis):等待被join的线程的时间最长为millis毫秒。如果到时候还没有结束则不再等待。

后台线程

也称为守护线程或精灵线程,他是在后台运行的,如果所有前台线程都死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。

实例:将执行线程设置为后台线程,可以看到当所有前台线程死亡时,后台线程随之死亡。

public class DaemonThread {
    public static void main(String[] args) {
        Thread dt=new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }
        },"DaemonThread");
        //设置为守护线程
        dt.setDaemon(true);
        dt.start();

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
        //当主线程(前台线程运行完)程序也随之结束,守护线程也结束
    }
}

执行结果:主线程运行为完成之后守护线程随之死亡

注意:
前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令到做出响应,需要一定时间,而且要将某个线程设置为后台线程,必须在该线程启动之前设置,否则会引发IllegalThreadStateException异常。

线程睡眠:sleep

如果需要让当前执行正在执行的线程暂停一段时间,并进入阻塞状态,则可用Thread类的静态方法sleep()来实现。
static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。在睡眠时间段,该线程不会获得任何执行的机会,即使系统中已经没有其他可执行的线程。
线程让步:yield

yield()方法和sleep方法类似。也是静态方法,也可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将线程转入就绪状态。事实上,当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。

public class YieldThread extends Thread{
    public YieldThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName()+" "+i);
            if(i==20){
                Thread.yield();
            }
        }
    }
    public static void main(String[] args) {
        YieldThread th1 = new YieldThread("th1");
        YieldThread th2 = new YieldThread("th2");
        th1.setPriority(Thread.MAX_PRIORITY);
        th1.start();
        th2.setPriority(Thread.MIN_PRIORITY);
        th2.start();
    }
}

å¨è¿éæå¥å¾çæè¿°

sleep和yield的区别

  • sleep不会理会线程的优先级,yield会有优先级;
  • sleep将线程转入阻塞状态,yield将线程转入就绪状态;
  • sleep方法要抛出InterruptedException异常,而yield不抛出;
  • sleep比yield具有更好的可移植性。

改变线程的优先级

每个线程执行时都具有一定的优先级,优先级高的获得较多的执行机会。没个线程默认的优先级与创建它的父线程相同。默认情况下main线程和它创建的子线程都具有普通优先级。
Thread类提供了setPriority(int newPriority),getPriority方法来设置和返回执行线程的优先级。其中setPriority方法的参数可以是一个整数,范围是1~10之间。也可以用静态常量来表示:

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5

public class PriorityTest extends Thread{
    public PriorityTest(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName()+"其优先级:"+getPriority()+",循环变量的值:"+i);
        }
    }

    public static void main(String[] args) {
        //改变主线程的优先级
        Thread.currentThread().setPriority(6);
        for (int i = 0; i < 30; i++) {
            System.out.println("主线程优先级:"+Thread.currentThread().getPriority()+",循环变量的值:"+i);
            if(i==10){
                PriorityTest low = new PriorityTest("低级");
                low.start();
                System.out.println("低级创建之初的优先级:"+low.getPriority());
                //设置该线程优先级为最低
                low.setPriority(Thread.MIN_PRIORITY);

            }
            if(i==20){
                PriorityTest high = new PriorityTest("高级");
                high.start();
                System.out.println("高级创建之初的优先级:"+high.getPriority());
                //设置该线程优先级为最高
                high.setPriority(Thread.MAX_PRIORITY);
            }
        }
    }
}

优先级高的线程具有更改概率的执行权。

å¨è¿éæå¥å¾çæè¿°

线程同步

线程安全问题

模拟售票问题,一共有100张票,三个窗口来销售这100张票;

public class TicketSaledDemo implements Runnable {
    //定义票数
    int ticket=100;
    //售票执行体
    @Override
    public void run() {
    //模拟网络延迟
     try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
        while (true){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
            }else{
                    break;
                }
        }
    }

  public static void main(String[] args) {
        TicketSaledDemo ticket = new TicketSaledDemo();
        //创建三个窗口(线程)
        Thread win1 = new Thread(ticket, "窗口1");
        Thread win2 = new Thread(ticket, "窗口2");
        Thread win3 = new Thread(ticket, "窗口3");
        win1.start();
        win2.start();
        win3.start();
    }
}

出现以下两个问题:

  1. 同票
  2. 负票

运行结果出现了线程安全问题,线程安全问题的条件:

  • 多线程环境
  • 要有共享共享数据
  • 有多条语句操作共享数据

先说出现相同票数的原因,这是因为代码中的 ticket--不是原子操作,那什么是原子操作呢?原子操作就是只有一条语句的操作,而事实上 ticket--是分为三步进行操作的。可以分为:

//存原值
int t=ticket;
//运算
t=ticket-1;
//刷新旧值
ticket=t;

所以个代码不是原子操作,这就有可能导致了窗口一线程拿到了ticket=9时,还没来得及进行减1,win2线程就开始执行了,窗口二线程拿到的ticket值也是9,所以就导致了最后得到了相同的票数。

出现负票的原因,这是由于线程的随机性导致的,代码中if(ticket>0),线程的切换是随机的,比如当ticket=1时,win1线程先进入到if语句中,还没来得及执行减1,此时CPU的执行权限就被Win2线程抢去了,这时ticket还是1,所以win2也能进入if语句,这两个线程都进入到了然后不管谁先执行减1,最后两个线程都会执行,所以就减了2,最后结果得到了第-1张票。

出现问题
出现这两个问题的都是因为run方法的执行体不具有同步性,因为有可能多个线程并发执行run方法,就导致了线程不安全。

解决方法:同步代码块(也就是线程安全问题)

为了解决这个问题,Java的多线程支持引入了同步监视器,使用同步监视器的通用方法就是同步代码块,格式如下:

synchronized(obj){
	//要同步的代码块
}

java程序允许任何对象作为同步监视器,同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,通常推荐使用可能并发访问的共享资源充当同步监视器。

public class TicketSaledDemo implements Runnable {
    static Object obj=new Object();
    //定义票数
    static int ticket=100;
    //售票执行体
    @Override
    public void run() {

            while (true){
                synchronized (obj){
                    if(ticket>0){
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
                    }else{
                            break;//结束死循环
                            }
            }
            //同步代码块结束,线程释放锁
        }
    }
}

想到于加锁——>票数减1——>释放锁,任何线程在修改指定资源之前,首先对该资源进行加锁,在加锁到释放锁期间,其他线程无法修改该资源,当修改完成之后,释放锁。通过这种方式就可以保证在并发过程中任意时刻只有一个线程可以进入同步代码块。

加锁(同步)相当于:当循环进入一个线程时 此时加锁不能再有线程进入,当里面的线程执行完出来时,再进去一个,以此类推

同步方法

与同步代码块对应Java的多线程安全还包含了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则称为同步方法。同步方法无需显式指同步监视器,默认为this。

public class TicketSaledDemo implements Runnable {
    static Object obj=new Object();
    //定义票数
    static int ticket=100;
    //售票执行体
    @Override
    public void run() {
            while (true){
            excute();
            //同步代码块结束,线程释放锁
        }
    }
    private synchronized void excute() {
        if(ticket>0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
        }
    }
}

线程安全是以降低程序的运行效率作为代价,为了减少线程安全所带来的负面影响可采用如下策略:

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步。
  • 如果可变类有两种运行环境,单线程和多线程,则应该为该可变类提供两个版本,即线程不安全和线程安全。在单线程中使用线程不安全版本以保证性能,在多线程环境使用线程安全版本。

释放同步监视器的锁定

  • 当前线程的同步方法、同步代码块执行结束,释放当前线程的同步监视器;
  • 当前线程在同步代码块遇到break、return终止了该代码块、该方法的继续执行,释放。
  • 当前线程在同步代码块遇到未处理的Error,Exception导致该代码块、该方法的异常结束,释放。
  • 当前线程在同步代码块遇到了同步监视器对象的wait()方法,则当前线程暂停,释放。

如下情况不会释放:

  • 在同步代码块遇到Thread.sleep()、Thread.yield()、当前线程的suspend()(挂起该线程,相反放下resume),不会释放同步监视器;

同步锁(Lock)

从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁由Lock对象充当。

在实现线性安全的控制中,比较常用的是ReentranLock(可重入锁),使用该对象可以显式地加锁、释放锁。

public class TicketSaledDemo implements Runnable {
    //定义锁对象
    private final ReentrantLock lock=new ReentrantLock();
    //定义票数
    static int ticket=100;
    //售票执行体
    @Override
    public void run() {
            while (true){
            excute();
        }
    }
    //需要同步的方法
    private  void excute() {
        //加锁
        lock.lock();
        if(ticket>0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
                //释放锁
                lock.unlock();
            }
        }
    }
}

使用ReentranLock对象来进行同步,加锁和释放锁出现在不同的作用范围时,通常建议使用finally块来确保在必要时候释放锁。
ReentranLock锁具有可重入性,一个线程可以对已被加锁的ReentranLock锁再次加锁,ReentranLock对象会为之一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock来释放锁,所以一段被锁保护的代码可以调用两一个被相同锁保护的方法。

死锁

先来看一个案例:

public class ObjUtils {
    //创建两个同步线程锁
    public static final Object objA=new Object();
    public static final Object objB=new Object();
}
public class LockTest extends Thread{
    private boolean b;

    public LockTest(boolean b) {
        this.b=b;
    }

    @Override
    public void run() {
        if(b){
            synchronized (ObjUtils.objA){
                System.out.println("trueA进来了");
                synchronized (ObjUtils.objB){
                    System.out.println("trueB进来了");
                }
            }
        }else {
            synchronized (ObjUtils.objB){
                System.out.println("falseB进来了");
                synchronized (ObjUtils.objA){
                    System.out.println("falseA进来了");
                }
            }
        }

    }

    public static void main(String[] args) {
        LockTest lockA = new LockTest(true);
        LockTest lockB = new LockTest(false);
        lockA.start();
        lockB.start();

    }
}

å¨è¿éæå¥å¾çæè¿°

这种现象称为死锁,两个线程执行体都需要两个锁才能执行完,而当第一个线程的拿到了A锁时,第二个线程也拿到了B锁,而第一个线程还需要B锁,第二个线程还需要A锁才能执行,由于执行体还没有执行完,两个线程都不会释放锁,就陷入了僵持的状态。

同样的例子还有:张三说你把书给我我把花给你,李四说你把话给我,我把书给你。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值