[TOC]7
线程补充
线程状态
调用线程有关的方法使线程进入不同的状态。可以通过调用Thread.getState()来获取线程运行的状态。
线程组
可以把线程归属到一个线程组中,线程组可以有线程对象,也可以有线程组,组中还可以有线程。类似嵌套一样。
1 | public class SingleEnum { |
线程组可以有多级关联,也就是父对象有子对象,子对象再创建子对象。
线程组自动归属
如果创建一个线程组却没有指定所属的线程组之后,则线程组会自动归属到当前线程的线程组中。
[TOC]7
调用线程有关的方法使线程进入不同的状态。可以通过调用Thread.getState()来获取线程运行的状态。
可以把线程归属到一个线程组中,线程组可以有线程对象,也可以有线程组,组中还可以有线程。类似嵌套一样。
1 | public class SingleEnum { |
线程组可以有多级关联,也就是父对象有子对象,子对象再创建子对象。
如果创建一个线程组却没有指定所属的线程组之后,则线程组会自动归属到当前线程的线程组中。
[TOC]
timer类的主要作用就是设置计划任务,但封装任务的类却是TimerTask类,
该方法的作用是在指定日期执行一次某一任务。
代码如下
1 | public class ThreadTimer { |
上述代码是在一秒之后打印当前时间。
虽然指定了执行时间,都是如果多个任务执行的话会先执行完前面的任务才会执行后面的任务。
该方法时指定日期之后按照间隔周期无限的执行某一任务。
1 | timer.schedule(new TimerTask() { |
1 | 运行中。。。2019-09-03 17:37:36 |
如果传入的时间早于当前时间,则立即执行任务。
延时delay之后执行一次任务
延时delay之后执行任务,且每隔period循环一次。
该方法与schedule
区别的只有延时,
使用schedule方法:如果执行任务的时间没有被延时,那么下次任务的执行时间参考上一次任务的开始时间来计算。
此方法则参考上一次任务结束的时间。
有延时则没有区别。
TimerTask的cancel方法时将自身从任务队列中移除
和Timertask的cancel不同,Timer的cancel的作用是将全部任务清空。
在多线程的世界里,很多看起来很正常的代码可能会出现不一样的效果。比如单例模式。如果在设计不佳的情况下,就有可能在多线程环境下出现问题。
饿汉模式/立即加载
饿汉模式是在方法调用前就已经创建了实例。
1 | public class MyObject{ |
懒汉模式/延迟加载
懒汉模式即在使用的时候加载
1 | class MyObject{ |
但是,这两种模式在多线程下都是线程不安全的。即有可能创建多个实例。
解决方案
在getInstance方法中加入synchronized即可,但是这种方法效率比较低下。
在方法中使用synchronized
包裹代码块,这种方案跟同步方法差不多。而且效率也比较低。
1 | class SingleModeService { |
将判断是否为空放到同步代码外,这样效率会提高一些。
1 | class SingleModeService { |
使用双重检验锁的原因还是因为前面的同步代码块其实并不安全。主要是因为判空是在同步代码块外面,所以有可能刚刚判断为空,另外一条线程立马就创建了一个实例,这时候已经过了判断空值的行。所以在进入到同步代码块之前还是有可能已经不为空了。
1 | public class SingleMode { |
静态内部类可以解决线程安全的问题。
静态代码块中的代码在使用类的时候就已经执行了。所以可以使用这个特性来实现单例设计模式。
1 | public class SingleMode { |
在《Java编程思想》中推荐使用的方式。这也是由于枚举的特性。
1 | public class SingleEnum { |
输出的结果也是正确的。
[TOC]
在JDK1.5的版本中新加入了很多特性。其中就有ReentrantLock
。ReentrantLock
比synchronized
更灵活。功能也更强大。学习线程的过程中也是不可避免的。new
一个ReentrantLock
返回一个Lock
接口对象。
通过lock
加锁。unlock
解锁。在lock与unlock之间的代码就是同步区域。
关键字synchronized
与wait
和notify
方法相结合可以实现等待通知模式,类ReentrantLock
也可以实现同样的功能,但是要借助于Condition
对象,它有更好的灵活性,比如实现多路通知功能,也就是说在一个lock对象创建多个Condition
实例,线程对象可以注册在指定的Condition
中,从而可以有选择的进行线程通知,在调度线程上更灵活。
在notify中,被通知的线程是JVM随机选择的。但使用ReentrantLock
结合Condition
可以实现选择性通知。
代码如下:
1 | public class ThreadCondition { |
其中Condition
的await
等于Object
的wait
,signal
等于notify
。signalAll
等于notifyAll
。
1 | public class ThreadConditionSelect { |
在这代码中最终只有A被唤醒。
通过这种方式我们可以唤醒指定种类的线程。
锁lock分为公平锁与非公平锁。公平锁表示线程获取锁的顺序是安装线程加锁的顺序来分配的,即FIFO先进先出的顺序。而非公平锁则是抢占。随机获取锁。所以可能会有一些线程一直拿不到锁。
通过创建ReentrantLock的构造函数,传入Boolean值设置锁的类型。
通过源码,我们可以知道,在不传入值的情况下默认是非公平锁。
1 | public class ThreadGPLock { |
通过结果我们可以看出,当锁是公平锁时,谁先等待那么谁就先获取到锁。而非公平锁则是相互竞争。
getHoldCount()
的作用时查询当前线程锁保持锁定的个数。也就是调用lock()方法的次数。getQueueLength()
是返回正在等待获取锁的线程数,表示没有调用await的线程。getWaitQueueLength()
与getQueueLength()
相反,它表示已经调用了await方法的线程数。hasQueuedThread(Thread)
的作用是查询指定线程释放在等待获取此锁定。HasQueuedThreads()
的作用是查询是否有线程正在等待获取此锁定。hasWaiters(Condition condition)
查询是否有线程正在等待与此锁定有关的condition条件。
…
lockInterruptibly()
的作用是如果当前线程未被中断,则获取锁定。如果已经被中断则出现异常0
tryLock()
的作用是如果锁没被其他线程获取就获得锁。
tryLock(long time, TimeUnit unit)
在给定时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
ReentrantLock
类具有完全排他的效果,在同一时间下只有一个线程在执行lock方法后面的任务,虽然保证了安全性,但是效率相对低下。我们可以使用ReentrantReadWriteLock
读写锁,在某些不需要操作实例变量中,完全可以使用读写锁来提升代码运行效率。
读写锁有两个锁,一个是读相关的锁,叫做共享锁,另一个是写操作相关的锁,也称排他锁。写锁与任何锁都互斥。
同一个时刻读锁允许多个线程获取锁,而写锁只允许一个线程获取锁
1 | public class ThreadReadWriteLock { |
结果如下:
1 | 获取读锁:Thread-0 |
通过lock.readLock().lock()加一个读锁。
通过lock.writeLock().lock()加一个写锁
如果这个锁是一个ReentrantLock锁,则同一时刻只能允许一个线程运行,所以如果有10个线程。每个线程睡眠0.5秒,十个线程则需要睡5秒。在使用读锁之后,所有线程都可以同时运行。
可见效率要高很多。
写线程与ReentrantLock基本上一致。
[TOC]
在前面线程方法中我们遇到了wait();notify();方法。接下来我们讲讲这些方法。
wait
方法的作用是将当前执行代码的线程进行等待。但wait
并不是Thread
的方法而是Object
的方法。至于为什么不放在Thread
请看这里为什么wait方法定义在Object,而不是Thread。这篇文章讲的很清楚了。wait
方法是将当前线程放入“预执行队列”中。并且在wait
所在的代码处停止执行。直到接到通知或中断为止。同时需要注意。在调用wait
方法之前。线程需要获得该对象的对象对象级别锁。如果调用wait
时没有获取到锁则抛出异常。
带参数的方法表示超过这个时间就会自动唤醒。也就是等待多久。
notify()
;方法恰好与wait
相对。当线程使用wait
方法等待时可以使用notify()
将其唤醒。同理notify
也需要在同步方法或同步代码块中调用。notifyAll()
与notify()
的功能一样。只不过notify()
是唤醒一个线程。而notifyAll()
是唤醒所有线程。(针对同一个对象的所有线程)。
1 |
|
在线程run之后进入等待。同时主线程休眠0.5秒。0.5秒之后调用notify
唤醒线程。所以在调用wait方法之后wait
后面的代码就会停止执行。直到notify
执行完毕。当唤醒之后wai
t后面的代码继续执行
线程之间的状态切换
每个对象都有两个队列。一个是阻塞队列,另一个是就绪队列。当线程被唤醒之后会进入就绪队列。反之则进入阻塞队列。
当wait
执行完后。锁会自动释放。但是执行完notify
却不会自动释放锁。从之前的代码中我们知道。代码运行到wait
的时候会进行等待。所以wait
后面的代码就不执行了。刚开始的时候我以为代码停在这了。所以锁也没释放。其实当wait
运行完之后锁就会自动释放。将上面的代码稍加改动:
1 |
|
通过运行的结果表明,在调用wait
之后锁就已经被释放掉了。所有线程都要等待notify
的同步代码块执行完毕才会继续执行
等待/通知最经典的案例就是“生产者/消费者”模式。
1 |
|
1 | public class ThreadTestCP{ |
每次生产者生产完成之后唤醒等待线程。然后消费者进行消费。在这里如果出现多个消费者或多个生产者。那么线程就有可能出现假死状态。也就是所有线程都处于等待状态。可以将notify
换成notifyAll
解决假死状态。
同时这里的notify
是没有区分生产者/消费者的。也就是说生产者唤醒的也有可能是生产者。
简单来说就是:
消费者查看是否有数据。如果没有就等待。如果有就消费,随后唤醒生产者进行生产。
生产者被唤醒之后进行生产。生产之后唤醒消费者。数据载体可以是任何对象。只要生产者和消费者的数据载体是同一对象。
题目要求: 创建二十个线程。线程分为两种,一种输出“★“,另一种输出”☆“ 两种线程交替运行。
代码如下:
1 |
|
通过一个Boolean变量使两个线程变成互斥运行。使其其中一个线程进入睡眠。
很多时候主线程创建并启动子线程,如果子线程中有很多耗时的操作。往往主线程比子线程更早结束。如果在主线程中需要使用到子线程中的数据,这个时候就需要join
方法了。
join方法是使所属线程正常运行run方法。当前线程无限制等待。直到所属线程销毁。
1 | public class ThreadReplaceRun{ |
在执行过程中,子线程会休眠一段时间,在休眠完成之后主线程开始执行。其实join方法的底层也只是使用了wait方法;只是更方便我们使用。
注意 join方法不能与interrupt方法一起使用。
这两者在运行的效果上基本没什么区别。只是咋对待同步的处理上有点区别。由于join底层是使用wait进行等待的,所以具有释放锁的特点。
在前面我们都是使用的public static
变量的形式。所有线程都使用同一个public static
变量。
而ThreadLocal
则是为每一个线程提供共享变量的功能。ThreadLocal
可以被视为给线程存放数据的箱子
。
我们可以使用一个ThreadLocal类为每一个线程存入数据。
1 |
|
不难发现,每个线程get的都只是那个线程存入的数据。不能获取其他线程的数据。
在没有进行set之前任何一个线程get返回的都是null。如果想设置一个默认值。我们可以继承ThreadLocal,并实现inittialValue()
方法返回默认值
[TOC]
线程的状态又分为五种:
1 | graph TD |
判断当前线程是否是中断、停止状态,执行后将状态标志清除为false
,静态方法。可以直接调用
判断当前线程是否是中断、停止状态。但不清除状态标志。非静态方法。
将状态标记为中断,用于线程退出。在run
方法中使用interrupted
判断线程状态。如果返回true
则return
或抛出异常。
将Running状态转变为Runnable状态。把线程CPU让给其他线程,让出时间不确定,有可能上一秒让出下一秒又获取到。
使线程休眠多少毫秒,参数为long
ms
join方法的作用是父线程等待子线程执行完成后再执行。换句话说就是将异步执行的线程合并为同步的线程。
这些方法并不是Thread类中的。而是Object类的方法。不过这些也是学习线程中不可或缺的。
wait()
方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程。此方法需要与锁一起使用。
与使用wait
的同一对象使用该方法即可唤醒处于等待的线程。
与notify
方法差不多,只不过notify
只唤醒当前线程。而notifyAll
则唤醒所有线程。
在线程中可以通过setPriority(int)
来设置线程优先级。子类的优先级与父类的优先级一致。
线程优先级从低到高为1~10。超过则抛出IllegalArgumentException()
异常
且线程优先级不代表一定会按照顺序执行。
在Java线程中有两种线程,一种是用户线程,另一种是守护线程。
一旦用户线程全都执行完毕之后,守护线程也会结束。没有用户线程时守护进程将自动销毁。
线程可以在执行start()
方法之前执行setDaemon(true)
变成守护进程。
当多个线程对一个对象实例或者一个对象实例变量访问的时候就有可能出现线程安全。
所有线程内部的私有变量都不会引起线程安全
synchronized
所声明的方法为线程安全。synchronized
使用:synchronized
可以在方法签名中声明。也可以声明代码块。
1 | public synchronized void mothed(){ |
最好不要使用String,或其他基本数据类型作为synchronized
所修饰的对象。因为
1 | String a1="aa"; |
由于常量池的原因。a1=a2。所以在synchronized(String)
中会被当成同一个对象。
使用synchronized
之后,异步方法会进行排队。可以看作原子级的方法。
只有共享变量/实例才需要同步。如果不是共享变量或实例则不需要同步。
对synchronized
声明的代码块,方法来说都具有重入的特性,也就是说synchronized
代码块或方法可以调用同一个对象的synchronized
而不会产生死锁。
使用synchronized
声明一个同步代码块比直接声明一个方法性能要好很多。同时使用一个对象实例比this
效率要高一些。使用synchronized(非this)
代码块中的程序与其他同步方法是异步的。因为他锁的对象不是this,如果需要同步的话需要锁住的对象是同一个。
注意:同步代码块放在非同步synchronized方法中进行声明,并不能保证调用方法的线程的执行顺序。所以非常容易产生“脏读”的问题。
也就是说synchronized
修饰方法时,锁住的对象是this
。所以synchronized
修饰的方法与synchronized(非this)
的代码块之间非常容易出问题。
synchronized
可以用到静态方法上,这样写是对当前*.java文件对应的Class类进行加锁。synchronized
加到静态方法上是对Class加锁。synchronized
加到非静态方法上是给对象加锁。如果有一个静态方法。同时有两个实例调用此方法。则该方法为同步方法。synchronized(Object.class)
等于synchronized
声明静态方法。
在同步代码块中如果锁对象发生了改变,会发生什么呢?锁对象还有用吗?
1 |
|
真实情况是:一旦锁发生了改变,且在改变之后又有新的线程进来。那么这两个线程的锁对象就不相等。就会变成异步方法。同时可能会产生“脏读”的情况。
1 | A1:start |
提示: 只要对象不变。就算对象的属性变了,运行的结果也是同步。
volatile
是为了确保公共堆栈中的变量与线程私有栈中的变量保持一致。使用了volatile
之后会从公共堆栈取值。
如图所示:
1 |
|
如果线程没有及时读到最新的值。那么就可能会出现死循环。
他会一直卡在while
循环中。因为在公共堆栈中已经修改了flag的值。但是在线程私有栈中却没有更新flag
的值
加了volatile
之后
1 |
|
运行结果:
1 | 进入run |
乍一看volatile似乎可以解决多线程下的“脏读”问题了。但是其实上并不是。
volatile
是线程同步的轻量级实现。所以volatile
的性能比synchronized
要好。但是随着JDK版本的提升。synchronized
效率也的到了加强。volatile
不会发生阻塞。synchronized
会发生阻塞。volatile
能保证数据可见性。但是不能保证原子性。而synchronized
可以保证原子性。也可以间接保证可见性。因为他会将私有内存与公共内存中的数据做同步。volatile
解决的是变量在多个线程中的可见性。而synchronized
解决的是多个线程之间访问资源的同步性。1 | public class ThreadTest3 { |
在所有的结果中都没有count=10000
的这条结果。这个问题是因为count++
并不是一个原子性操作。
解决: 有一点需要注意。也就是count
是static
所修饰的。而且在main
方法中是new
了100个线程。所以我们在使用synchronized
的时候锁对象一定是一个Class
;
如将synchronized
添加到static方法中。或者synchronized
代码块中的锁对象是一个当前类的Class
。
1 |
|
从结果中可以看出虽然顺序是随机的。但是一定有一条数据是count等于10000;
方案二 使用原子类
在前面我们使用了synchronized
,这次我们不用synchronized
而使用原子类。
原子类可以将i++这样的操作当成一个原子操作。而不使用锁。
1 |
|
可以看到,最终结果也有10000。针对与i++这样的操作可以使用原子类来做到线程安全。
还有个问题 虽然最终的结果已经是10000了,但是他不是顺序的。这是因为方法与方法之间不是同步的。将代码稍微改一下:将输出语句添加到同步块中,这样结果就是顺序的了。
我们通常会使用一个枚举表示状态,如
1 | enum State{ |
我们可以声明一个枚举变量表示一个状态
但是: 如果我要表示多个状态怎么办。这种方式只能解决一个状态的问题。
枚举的多个状态可以通过二进制的位运算来实现。
原理是什么
1 | int state=0;//表示一个状态 |
state
里面。如1的二进制是0001,2的二进制为0010。包含两个状态的值为0011。state
是否包含此状态,如1000&0101=1000,因为我们是通过每一位是否包含1来判断是否包含这个状态。所以每一个状态必须满足2^n。1 | enum State{ |
1 | private State state =0;//状态 |
1 |
|
1 | private boolean includeState(State state) { |
1 | private removeState(State state) { |