[TOC]
线程基础
线程状态
线程的状态又分为五种:
- New:新建状态
- Runnable:就绪状态
- Running:运行状态
- Blocked:阻塞状态
- Dead:死亡状态
1 | graph TD |
线程方法
interrupted()
判断当前线程是否是中断、停止状态,执行后将状态标志清除为false
,静态方法。可以直接调用
isInterrupted()
判断当前线程是否是中断、停止状态。但不清除状态标志。非静态方法。
interrupt()
将状态标记为中断,用于线程退出。在run
方法中使用interrupted
判断线程状态。如果返回true
则return
或抛出异常。
yield()
将Running状态转变为Runnable状态。把线程CPU让给其他线程,让出时间不确定,有可能上一秒让出下一秒又获取到。
sleep()
使线程休眠多少毫秒,参数为long
ms
join()
join方法的作用是父线程等待子线程执行完成后再执行。换句话说就是将异步执行的线程合并为同步的线程。
wait/notify/notifyAll方法
这些方法并不是Thread类中的。而是Object类的方法。不过这些也是学习线程中不可或缺的。
wait()
wait()
方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程。此方法需要与锁一起使用。
notify
与使用wait
的同一对象使用该方法即可唤醒处于等待的线程。
notifyAll
与notify
方法差不多,只不过notify
只唤醒当前线程。而notifyAll
则唤醒所有线程。
线程优先级
在线程中可以通过setPriority(int)
来设置线程优先级。子类的优先级与父类的优先级一致。
线程优先级从低到高为1~10。超过则抛出IllegalArgumentException()
异常
且线程优先级不代表一定会按照顺序执行。
守护线程
在Java线程中有两种线程,一种是用户线程,另一种是守护线程。
一旦用户线程全都执行完毕之后,守护线程也会结束。没有用户线程时守护进程将自动销毁。
线程可以在执行start()
方法之前执行setDaemon(true)
变成守护进程。
对象及变量并发访问
当多个线程对一个对象实例或者一个对象实例变量访问的时候就有可能出现线程安全。
方法内的变量为线程安全
所有线程内部的私有变量都不会引起线程安全
synchronized
synchronized
所声明的方法为线程安全。synchronized
使用:synchronized
可以在方法签名中声明。也可以声明代码块。
1 | public synchronized void mothed(){ |
最好不要使用String,或其他基本数据类型作为synchronized
所修饰的对象。因为
1 | String a1="aa"; |
由于常量池的原因。a1=a2。所以在synchronized(String)
中会被当成同一个对象。
使用synchronized
之后,异步方法会进行排队。可以看作原子级的方法。
只有共享变量/实例才需要同步。如果不是共享变量或实例则不需要同步。
synchronized 重入
对synchronized
声明的代码块,方法来说都具有重入的特性,也就是说synchronized
代码块或方法可以调用同一个对象的synchronized
而不会产生死锁。
synchronized 同步代码块
使用synchronized
声明一个同步代码块比直接声明一个方法性能要好很多。同时使用一个对象实例比this
效率要高一些。使用synchronized(非this)
代码块中的程序与其他同步方法是异步的。因为他锁的对象不是this,如果需要同步的话需要锁住的对象是同一个。
注意:同步代码块放在非同步synchronized方法中进行声明,并不能保证调用方法的线程的执行顺序。所以非常容易产生“脏读”的问题。
也就是说synchronized
修饰方法时,锁住的对象是this
。所以synchronized
修饰的方法与synchronized(非this)
的代码块之间非常容易出问题。
synchronized静态同步
synchronized
可以用到静态方法上,这样写是对当前*.java文件对应的Class类进行加锁。synchronized
加到静态方法上是对Class加锁。synchronized
加到非静态方法上是给对象加锁。如果有一个静态方法。同时有两个实例调用此方法。则该方法为同步方法。synchronized(Object.class)
等于synchronized
声明静态方法。
锁对象改变
在同步代码块中如果锁对象发生了改变,会发生什么呢?锁对象还有用吗?
1 |
|
真实情况是:一旦锁发生了改变,且在改变之后又有新的线程进来。那么这两个线程的锁对象就不相等。就会变成异步方法。同时可能会产生“脏读”的情况。
1 | A1:start |
提示: 只要对象不变。就算对象的属性变了,运行的结果也是同步。
volatile
volatile
是为了确保公共堆栈中的变量与线程私有栈中的变量保持一致。使用了volatile
之后会从公共堆栈取值。
如图所示:
1 |
|
如果线程没有及时读到最新的值。那么就可能会出现死循环。
他会一直卡在while
循环中。因为在公共堆栈中已经修改了flag的值。但是在线程私有栈中却没有更新flag
的值
加了volatile
之后
1 |
|
运行结果:
1 | 进入run |
volatile与synchronized
乍一看volatile似乎可以解决多线程下的“脏读”问题了。但是其实上并不是。
volatile
是线程同步的轻量级实现。所以volatile
的性能比synchronized
要好。但是随着JDK版本的提升。synchronized
效率也的到了加强。- 多线程访问
volatile
不会发生阻塞。synchronized
会发生阻塞。 volatile
能保证数据可见性。但是不能保证原子性。而synchronized
可以保证原子性。也可以间接保证可见性。因为他会将私有内存与公共内存中的数据做同步。volatile
解决的是变量在多个线程中的可见性。而synchronized
解决的是多个线程之间访问资源的同步性。- 线程安全包含原子性和可见性两个方面。Java的同步机制都是围绕这两个方面来确保线程安全的。
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了,但是他不是顺序的。这是因为方法与方法之间不是同步的。将代码稍微改一下:将输出语句添加到同步块中,这样结果就是顺序的了。