本文目录:
1.几个基本的概念
2.创建线程的两种方法
3.线程相关的常用方法
4.多线程安全问题和线程同步
4.1 多线程安全问题
4.2 线程同步
4.3 同步代码块和同步函数的区别以及锁是什么
4.4 单例懒汉模式的多线程安全问题
5.死锁(DeadLock)
本文涉及到的一些概念,有些是基础知识,有些在后文会展开详细的说明。
还需需要明确的一个关键点是:CPU对就绪队列中每个线程的调度是随机的(对我们人类来说),且分配的时间片也是随机的(对人类来说)。
Java中有两种创建线程的方式。
创建线程方式一:
例如下面的代码中,在主线程main中创建了两个线程对象,先后并先后调用start()开启这两个线程,这两个线程会各自执行MyThread中的run()方法。
上面的代码执行时,有三个线程,首先是主线程main创建2个线程对象,并开启这两个线程任务,开启两个线程后主线程输出"main thread over",然后main线程结束。在开启两个线程任务后,这两个线程加入到了就绪队列等待CPU的调度执行。如下图。因为每个线程被cpu调度是随机的,执行时间也是随机的,所以即使mt1先开启任务,但mt2可能会比mt1线程先执行,也可能更先消亡。
创建线程方式二:
这两种创建线程的方法,无疑第二种(实现Runnable接口)要好一些,因为第一种创建方法继承了Thread后就无法继承其他父类。
Thread类中的方法:
Object类中的方法:
这里的某个线程池是由锁对象决定的。持有相同锁对象的线程属于同一个线程池。见后文。
一般来说,wait()和唤醒的notify()或notifyAll()是成对出现的,否则很容易出现死锁。
sleep()和wait()的区别:(1)所属类不同:sleep()在Thread类中,wait()则是在Object中;(2)sleep()可以指定睡眠时间,wait()虽然也可以指定睡眠时间,但大多数时候都不会去指定;(3)sleep()不会抛异常,而wait()会抛异常;(4)sleep()可以在任何地方使用,而wait()必须在同步代码块或同步函数中使用;(5)最大的区别是sleep()睡眠时不会释放锁,不会进入特定的线程池,在睡眠时间结束后自动苏醒并继续往下执行任务,而wait()睡眠时会释放锁,进入线程池,等待notify()或notifyAll()的唤醒。
java.util.concurrent.locks包中的类和它们的方法:
线程安全问题是指多线程同时执行时,对同一资源的并发操作会导致资源数据的混乱。
例如下面是用多个线程(窗口)售票的代码。
执行结果大致如下:
以上代码的执行过程大致如下图:
共开启了4个线程执行任务(不考虑main主线程),每一个线程都有4个任务:
这四个任务的共同点也是关键点在于它们都操作同一个资源Ticket对象中的num,这是多线程出现安全问题的本质,也是分析多线程执行过程的切入点。
当main线程开启t1-t4这4个线程时,它们首先进入就绪队列等待被CPU随机选中。(1).假如t1被先选中,分配的时间片执行到任务②就结束了,于是t1进入就绪队列等待被CPU随机选中,此时票数num自减后为99;(2).当t3被CPU选中时,t3所读取到的num也为99,假如t3分配到的时间片在执行到任务②也结束了,此时票数num自减后为98;(3).同理t2被选中执行到任务②结束后,num为97;(4).此时t3又被选中了,于是可以执行任务③,甚至是任务④,假设执行完任务④时间片才结束,于是t3的打印语句打印出来的num结果为97;(5).t1又被选中了,于是任务④打印出来的num也为97。
显然,上面的代码有几个问题:(1)有些票没有卖出去了但是没有记录;(2)有的票重复卖了。这就是线程安全问题。
java中解决线程安全问题的方法是使用互斥锁,也可称之为"同步"。解决思路如下:
(1).为待执行的任务设定给定一把锁,拥有相同锁对象的线程在wait()时会进入同一个线程池睡眠。
(2).线程在执行这个设了锁的任务时,首先判断锁是否空闲(即锁处于释放状态),如果空闲则去持有这把锁,只有持有这把锁的线程才能执行这个任务。即使时间片到了,它也不是释放锁,只有wait()或线程结束时才会安全地释放锁。
(3).这样一来,锁被某个线程持有时,其他线程在锁判断后就继续会线程池睡眠去了(或就绪队列)。最终导致的结果是,(设计合理的情况下)某个线程一定完整地执行完一个任务,其他线程才有机会去持有锁并执行任务。
换句话说,使用同步线程,可以保证线程执行的任务具有原子性,只要某个同步任务开始执行了就一定执行结束,且不允许其他线程参与。
让线程同步的方式有两种,一种是使用synchronized(){}
代码块,一种是使用synchronized关键字修饰待保证同步的方法。
使用同步之后,if(num>0)
、num--
、return num
和print(num)
这4个任务就强制具有原子性。某个线程只要开始执行了if语句,它就一定会继续执行直到执行完print(num),才算完成了一整个任务。只有完成了一整个任务,线程才会释放锁(当然,也可能继续判断while(true)并进入下一个循环)。
前面的示例中,同步代码块synchronized(obj){}中传递了一个obj的Object对象,这个obj可以是任意一个对象的引用,这些引用传递给代码块的作用是为了标识这个同步任务所属的锁。
而synchronized函数的本质其实是使用了this作为这个同步函数的锁标识,this代表的是当前对象的引用。但如果同步函数是静态的,即使用了static修饰,则此时this还没出现,它使用的锁是"类名.class"这个字节码文件对象,对于java来说,这也是一个对象,而且一个类中一定有这个对象。
使用相同的锁之间会互斥,但不同锁之间则没有任何影响。因此,要保证任务同步(原子性),这些任务所关联的锁必须相同。也因此,如果有多个同步任务(各自保证自己的同步性),就一定不能都使用同步函数。
例如下面的例子中,写了两个相同的sale()方法,并且使用了flag标记让不同线程能执行这两个同步任务。如果出现了多线程安全问题,则表明synchronized函数和同步代码块使用的是不同对象锁。如果将同步代码块中的对象改为this后不出现多线程安全问题,则表明同步函数使用的是this对象。如果为sale2()加上静态修饰static,则将obj替换为"Ticket.class"来测试。
以下是执行结果中的一小片段,出现了多线程安全问题。而如果将同步代码块中的obj改为this,则不会出现多线程安全问题。
单例饿汉式:
单例懒汉式:
当多线程操作单例饿汉式和懒汉式对象的资源时,是否有多线程安全问题?
以上面的代码为例。当多线程分别被CPU调度时,饿汉式中的getInstance()返回的s,s是final属性修饰的,因此随便哪个线程访问都是固定不变的。而懒汉式则随着不同线程的来临,不断new Single()
,也就是说各个线程