文章目录
什么是线程安全
线程安全的诱因有两点:
- 存在共享数据
- 存在多条线程操作共享数据
解决:通过互斥锁,保证同一时刻有且只有一个线程在操作共享数据
什么是互斥锁
将共享数据被当前正在访问的线程加上互斥锁后,在同一时刻,其他线程只能处于等待状态
synchronized关键字
作用:保证同一时刻,只有一个线程可以执行某方法,或某代码块
保证可见性,也就是线程操作共享数据被其他线程所看到,完全可替代volatile功能
synchronized的三种应用方式
修饰实例方法:对当前实例加锁,进入实例方法前要获取当前实例的锁
修饰静态方法:对当前类对象加锁,进入静态方法前要获取当前类对象的锁
修饰代码块:对指定对象加锁,进入代码块要获取给定对象的锁
synchronized作用于实例方法
## 上述代码开启两个线程操作同一个共享资源(i)
public class AccountingSync implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
/**
* 输出结果:
* 2000000
*/
}
java的线程同步锁可以是任意对象,一个对象只有一把锁
synchronized修饰的是实例方法increase,当前线程的锁便是实例对象instance
当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法(一个对象只有一把锁),但是能访问该实例对象的其他非synchronized方法
下属代码是错误的。
使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁
public class AccountingSyncBad implements Runnable{
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncBad());
//new新实例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
//join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
t1.join();
t2.join();
System.out.println(i);
}
}
synchronized修饰静态方法
当修饰静态方法,锁就是当前类的class对象锁。
class对象锁可以控制静态成员的并发操作
public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 作用于静态方法,锁是当前class对象,也就是
* AccountingSyncClass类对应的class对象
*/
public static synchronized void increase(){
i++;
}
/**
* 非静态,访问时锁不一样不会发生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncClass());
//new心事了
Thread t2=new Thread(new AccountingSyncClass());
//启动线程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
因为两个线程一个操作的实例对象锁,一个操作的是class对象锁,故会发生线程安全问题
synchronized同步代码块
- 在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分
- 如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其他耗时操作....
//使用同步代码块对变量i进行同步操作,锁对象为instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
使用this对象,class对象作为锁
```java
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class对象锁
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
synchronized底层原理
JVM中的同步基于 进入和退出管程对象(Monitor)实现.无论是显示同步指令(有明确的 monitorenter 和 monitorexit 指令,即同步代码块),还是隐式同步
同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的
Java对象头与Monitor
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下
- 实例变量:存放类的属性数据信息,这部分内存按4字节对齐
- 填充数据:虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
- Java头对象:jvm中采用2个字来存储对象头,synchronized使用的锁对象是存储在Java对象头里的
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
32/64bit | Class Metadata Address | 指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
|锁对象| 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
|–|--|
| 无锁对象 | 对象HashCode | 对象分代年龄| 0 | 01 |
除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构
Java虚拟机对synchronized的优化
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级
无锁状态
假定不存在多线程竞争
偏向锁
Java6之后加入的新锁,它是对锁操作的优化手段。
锁对象总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能
所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁
轻量级锁
轻量级锁能够提升程序性能的依据是对绝大部分的锁,在整个同步周期内都不存在竞争,注意这是经验数据
轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁
自旋锁
基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高
自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区
锁消除-无锁状态
JVM在JIT(即使编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间.。
如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除
/**
* Created by zejian on 2017/6/4.
* Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
* 消除StringBuffer同步锁
*/
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
//因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
for (int i = 0; i < 10000000; i++) {
rmsync.add("abc", "123");
}
}
}
synchronized的关键点
synchronized的可重入性
当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态
但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功
synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this,当前实例对象锁
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1
线程中断与synchronized
线程中断
Java提供了以下三个有关线程中断的方法
//中断线程(实例方法)
public void Thread.interrupt();
//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();
//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
当一个线程处于阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,此时会抛出一个InterruptedException的异常,同时中断状态会改为非中断状态
public class InterruputSleepThread3 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
//while在try中,通过异常中断就可以退出run循环
try {
while (true) {
//当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
System.out.println("Interruted When Sleep");
boolean interrupt = this.isInterrupted();
//中断状态被复位
System.out.println("interrupt:"+interrupt);
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
//中断处于阻塞状态的线程
t1.interrupt();
/**
* 输出结果:
Interruted When Sleep
interrupt:false
*/
}
}
处于运行期且非阻塞的状态的线程,这种情况下,直接调用Thread.interrupt()中断线程是不会得到任响应的
public class InterruputThread {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
System.out.println("未被中断");
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
/**
* 输出结果(无限执行):
未被中断
未被中断
未被中断
......
*/
}
}
处于非阻塞状态的线程需要我们手动进行中断检测并结束程序
public class InterruputThread {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
//判断当前线程是否被中断
if (this.isInterrupted()){
System.out.println("线程中断");
break;
}
}
System.out.println("已跳出循环,线程中断!");
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
/**
* 输出结果:
线程中断
已跳出循环,线程中断!
*/
}
}
线程中断总结
一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位
另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)
中断与synchronized
线程的中断操作对于正在等待获取锁对象 的synchronized方法或代码块不起作用
/**
* Created by zejian on 2017/6/2.
* Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
*/
public class SynchronizedBlocked implements Runnable{
public synchronized void f() {
System.out.println("Trying to call f()");
while(true) // Never releases lock
Thread.yield();
}
/**
* 在构造器中创建新线程并启动获取对象锁
*/
public SynchronizedBlocked() {
//该线程已持有当前实例锁
new Thread() {
public void run() {
f(); // Lock acquired by this thread
}
}.start();
}
public void run() {
//中断判断
while (true) {
if (Thread.interrupted()) {
System.out.println("中断线程!!");
break;
} else {
f();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlocked sync = new SynchronizedBlocked();
Thread t = new Thread(sync);
//启动后调用f()方法,无法获取当前实例锁处于等待状态
t.start();
TimeUnit.SECONDS.sleep(1);
//中断线程,无法生效
t.interrupt();
}
}
等待唤醒机制与synchronized
-
等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常
-
也就是说notify/notifyAll和wait方法依赖于monitor对象,而synchronized关键字可以获取
monitor
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
sleep方法与wait方法的却别
- 调用wait方法后,会释放掉当前持有的锁对象,线程将被暂停,直到存在线程调用notify/notifyAll方法才可继续执行
- notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized()/synchronized方法执行结束后才自动释放锁
- sleep方法释放cpu资源但不会释放锁