打断park的线程:
public static void main(String[] args) throws InterruptedException {
Thread myThread = new Thread(()->{
System.out.println("park---->");
LockSupport.park();
System.out.println("unpark---->");
System.out.println("打断状态:" + Thread.currentThread().isInterrupted());
});
myThread.start();
Thread.sleep(1000);
myThread.interrupt();
}
输出:
park---->
unpark---->
打断状态:true
park中的线程被打断后,打断状态为true,注意:此时打断状态为true,在myThread线程中继续调用park将不会进入阻塞,如果想调用park就进入阻塞,则可以调用Thread.interrupted()
清除打断标记。
public static void main(String[] args) throws InterruptedException {
Thread myThread = new Thread(()->{
System.out.println("park---->");
LockSupport.park();
System.out.println("unpark---->");
System.out.println("打断状态:" + Thread.interrupted()); //注意此处修改为Thread.interrupted()
LockSupport.park();
System.out.println("我被park了吗????");
});
myThread.start();
Thread.sleep(1000);
myThread.interrupt();
}
输出:
park---->
unpark---->
打断状态:true
可以看到最后一句我被park了吗????
没有输出,说明被park住了
不再建议使用的方法
- stop(): 强制停止线程
- suspend(): 挂起(暂停)线程的运行
- resume(): 恢复线程运行
以上3个方法比较暴力,均会破坏同步代码块,不建议使用
主线程和守护线程
JVM中所有非守护线程运行完成即退出,即使守护线程未运行完,未运行完的守护线程会被强制结束。守护线程例子:垃圾回收线程。Tomcat的Acceptor线程和Poller线程也是守护线程,接收到shutdown命令后会被直接结束。
线程的状态
操作系统中的状态
- 初始状态:刚创建完成
- 可运行状态: 等待CPU调度
- 运行状态: 正在运行
- 终止状态:线程代码运行完成
- 阻塞状态:让出CPU,等待其它条件完成,如果不被唤醒,永远不会醒
JAVA中的六种状态
- NEW:新建状态,每调用start()方法,操作系统中的初始状态
- RUNNABLE:操作系统中的运行,可运行,阻塞(读取文件,调用线程API等)
- BLOCKED:等待满足条件,比如锁
- WAITING:等待其它线程运行完,比如join属于WAITING状态
- TIMED_WAITIING:Thread.sleep(long n)
- TERMINATED:终止状态
以下代码说明6种状态
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
System.out.println("running...");
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
int i =0;
while (true){
i++;
}
}
};
t2.start();
Thread t3 = new Thread("t3"){
@Override
public void run() {
System.out.println(this.getName() + " running...");
}
};
t3.start();
Thread t4 = new Thread("t4"){
@Override
public void run() {
synchronized (lock){
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5"){
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6"){
@Override
public void run() {
synchronized (lock){
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
Thread.sleep(1000);
System.out.println("t1: " + t1.getState());
System.out.println("t2: " + t2.getState());
System.out.println("t3: " + t3.getState());
System.out.println("t4: " + t4.getState());
System.out.println("t5: " + t5.getState());
System.out.println("t6: " + t6.getState());
}
输出:
t3 running...
t1: NEW
t2: RUNNABLE
t3: TERMINATED
t4: TIMED_WAITING
t5: WAITING
t6: BLOCKED
分析:
- t1线程创建了,但是没有调用start方法,因此是新建状态
- t2是一个无线循环,会一直循环下去,因此是运行状态
- t3打印完一行日志后代码运行完成,主线程睡眠1秒后t3线程执行完成了,所以是终止状态
- t4获取锁后sleep,是一个有限的等待,因此是TIMED_WAITING
- t5在等t2运行完成才会继续执行,由于t2是无线循环,因此t5一直在等t2完成,此时t5是WAITING
- t6和t4竞争同一个锁,t4拿到了这个锁未释放,t6在等待获取这个锁,因此是BLOCKED状态
synchronized
使用对象锁保证临界区的原子性
synchronized加在成员方法上,锁住的是this对象
synchronized加在静态方法上,锁住的是当前所在对象的类对象(Xxxx.class)
变量的线程安全
成员变量和静态变量线程安全
- 如果它们没有共享,则线程安全
- 如果他们共享了,则根据它们的状态是否改变分析:
- 如果只有读操作,则线程安全
- 如果有读写操作,这一部分是临界区,需要考虑线程安全
局部变量线程安全
- 局部变量是线程安全的
- 但局部变量的引用未必:
- 如果该对象没有逃离方法的作用域访问,则线程安全
- 如果该对象逃离了方法的作用域访问,则需要考虑线程安全
常见的线程安全类
- String 内部无法修改,属于不可变类
- Integer等包装类 内部无法修改,属于不可变类
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下面的类
注意:以上类每个方法内部代码执行都是原子的,但多个方法组合起来不一定
Monitor 监视器或者管程
java对象头
以32位虚拟机为例:
普通对象:
|--------------------------------------------|
| Object Header (64 bits) |
|--------------------------------------------|
| Mark Word(32 bit2)| Class Word (32 bits) |
|--------------------------------------------|
数组对象:
|---------------------------------------------------------------|
| Object Header (64 bits) |
|---------------------------------------------------------------|
| Mark Word(32 bit2)|Class Word(32 bits)|array length(32 bits) |
|---------------------------------------------------------------|
其中Mark Word结构为:
Mark Word:
- 第二行hashcode是对象hash码,age为分代年龄,biased代表是否是偏向锁,最后两位是加锁状态,Normal是对象的正常状态
- 第三行是偏向锁时的Mark word
- 第四行是轻量级锁时的Mark word
- 第五行是重量级锁时的Mark word,当一个object关联到一个monitor(操作系统持有)时,ptr_to_heavyweight_monitor会存储一个关联到的Monitor的指针,然后后面两位会修改为10,此时的hashcode和分代年龄等数据放入了Monitor中,解锁后再把数据还原
- 第六行是当前对象被标记为垃圾回收时的Mark word
用一个图说明一下:
上图中Owner存储的是线程ID,哪个线程获取到了锁,Owner就存储这个线程的ID。
锁分类
- 轻量级锁:多个线程会访问同一代码块,但运行时间是错开的,即使加了synchronized但不存在锁竞争,可以用轻量级锁来优化此种情形。
加轻量级锁时,会先在线程栈中生成一个Lock Record,让Lock Record中的对象引用(Object reference)指向对象,然后尝试使用CAS替换Object对象的Mark Word,将Mark Word的值存入锁记录(此时需要存入的值中最后两位就是00)。加锁成功后:
如果加锁不成功,可能有两种情况,一种是重入锁,当前线程本次操作不是第一次对这个对象加锁,此时也会生成一个新的Lock Record,数据不再存储Mark Word,而是null,此时有多少个Lock Record就代表重入了几次。另一种情况是出现了锁竞争,那么就进入锁膨胀流程。
解锁过程:如果Lock Record数据区有为null的记录,那么存在重入锁,直接删除一条Lock Record即可,如果没有了数据为null的Lock Record,说明是最初的那条加锁记录,此时使用CAS将Mark Word写回Object Header,删除锁记录,即可解锁。如果CAS失败了,说明由其它线程进入了锁膨胀流程,那么进入重量级锁解锁流程。
2. 锁膨胀
Thread-1持有Object的轻量级锁,此时Thread-2也对Object加锁,发现Object已经是轻量级锁了,这时出现的锁竞争,然后进行锁膨胀,过程如下
a. 先申请一个Monitor,然后锁,然后Object的Mark Word指向Monitor地址,最后两位改为01,Monitor的Owner地址设置为Thread-1的线程id
b. Thread-2自己加入到Monitor的EntryList进行等待
膨胀前:
膨胀后:
3. 重量级锁
重量级锁是有了竞争,并且有线程进入entrylist等待唤醒。
当Thread-1解锁时使用CAS还原Lock Record到Object的Mark Word,此时会失败,因为Object的Mark Word此时已经是Monitor的地址了,最后两位也修改成01了,这时就Thread-1就进入重量解锁流程,即将Ower清空,将EntryList中的线程唤醒。
-
自旋优化
重量级锁在获取Monitor不成功之后会进行自旋重试n次,如果此时其它线程释放锁,当前线程就能不进入阻塞队列获取到锁,避免2次上下文切换。这种优化适合多核CPU条件下,自旋也会占用CPU,单核CPU纯属浪费资源。如果自旋没有成功获取锁,就会进入EntryList,Java6后自适应,如果自旋能获取到锁的概率较大,会增加自旋次数,反之降低。Java7以后无法控制是否启用自旋功能。 -
偏向锁
轻量级锁每次发生锁重入的时候,都会使用CAS操作(检查),针对这种情况,可以使用偏向锁优化。第一次加锁时使用ThreadId替换Mark Word,后续都是直接检查是否是自己的ThreadId,不使用CAS操作。是否启用偏向锁在Mark Word的倒数第三位存储,0代表无,1代表使用了了偏向锁。默认偏向锁开启,但不会再程序启动时立即生效,要程序启动后一段时间后才会生效,若想立即生效,需要启动时加参数。
撤销偏向状态:
a. 当一个已经加了偏向锁的对象调用了了hasCode()方法后,会撤销偏向锁,原因是加偏向锁时Mark Word存入了线程Id用掉了23位,无法存储下25位的hashCode,因此智能撤销偏向锁,还原HashCode。
b. 当有其它线程也加偏向锁时,这时会升级成轻量级锁,偏向锁被撤销
a. 当一个已经加了偏向锁的对象调用了了hasCode()方法后,会撤销偏向锁,原因是加偏向锁时Mark Word存入了线程Id用掉了23位,无法存储下25位的hashCode,因此智能撤销偏向锁,还原HashCode。
c. 调用wait()/notify(),这两个是重量级锁用的,用到的话会把锁升级为重量级锁,自然就撤销了偏向锁
批量重偏向:
虽然被多个线程访问,但是没有竞争,这时偏向Thread-1的线程仍然有机会偏向Thread-2,重偏向会修改偏向锁中的ThreadId,默认情况下,当撤销偏向锁20次以后会发生重偏向
批量撤销:
当撤销偏向锁40次以后JVM认为不应该使用偏向锁,会将所有偏向锁撤回,不可偏向 -
锁消除
当一个锁不会逃逸出当前方法时,JVM会将其消除,如以下代码,两个方法运行1000w次,时间相差无几,就是JVM将method2的synchronized优化掉了。
public int x; public void method1(){ x++; } public void method2(){ Object o = new Object(); synchronized (o){ x++; } }