1. synchronized
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。
synchronized 锁什么?锁对象。
可能锁对象包括: this, 临界资源对象(多个线程都能访问到的对象),Class 类对象。
1.1 同步方法
1.1.1 加锁的目的
同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时,需同步执行。
synchronized 方法锁定的是当前对象。加锁的目的: 就是为了保证操作的原子性。
private int count = 0;
@Override
public /*synchronized*/ void run() {
System.out.println(Thread.currentThread().getName()
+ " count = " + count++);
}
public static void main(String[] args) {
Test_03 t = new Test_03();
for(int i = 0; i < 5; i++){
new Thread(t, "Thread - " + i).start();
}
}
1.1.2 静态同步方法
static synchronized 方法锁定的是锁的是当前类型的类对象。和synchronized(Test_02.class)一样。
public class Test_02{
private static int staticCount = 0;
public static synchronized void testSync4(){
System.out.println(Thread.currentThread().getName()
+ " staticCount = " + staticCount++);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void testSync5(){
synchronized(Test_02.class){
System.out.println(Thread.currentThread().getName()
+ " staticCount = " + staticCount++);
}
}
}
1.1.3 同步方法作用范围
可以看到在第一个线程调用方法m1()锁定当前对象后,在睡眠的时间内m2()m3()分别执行完毕。说明一个类中存在同步方法,其它线程可以同时调用该类中的其它非同步方法,或者其它不同锁对象的方法。
public class Test_04 {
Object o = new Object();
// 锁的当前对象
public synchronized void m1(){ // 重量级的访问操作。
System.out.println("public synchronized void m1() start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("public synchronized void m1() end");
}
public void m3(){
// 所得临界资源对象o
synchronized(o){
System.out.println("public void m3() start");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("public void m3() end");
}
}
public void m2(){
System.out.println("public void m2() start");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("public void m2() end");
}
public static class MyThread01 implements Runnable{
public MyThread01(int i, Test_04 t){
this.i = i;
this.t = t;
}
int i ;
Test_04 t;
public void run(){
if(i == 0){
t.m1();
}else if (i > 0){
t.m2();
}else {
t.m3();
}
}
}
public static void main(String[] args) {
Test_04 t = new Test_04();
new Thread(new Test_04.MyThread01(0, t)).start();
new Thread(new Test_04.MyThread01(1, t)).start();
new Thread(new Test_04.MyThread01(-1, t)).start();
}
}
1.1.5 脏读
m1()设置变量d的值,设置之前sleep睡眠2秒;m2()获取变量b的值。在主方法中启动一个线程设置d为100,接着通过m2()方法获取d值期望获取到100,而实际上在两秒过后我们才能获取到设置的100。 这里体现的即为脏读问题。
同步方法 - 多方法调用原子性问题(业务)
同步方法只能保证当前方法的原子性,不能保证多个业务方法之间的互相访问的原子性。
注意在商业开发中,多方法要求结果访问原子操作,需要多个方法都加锁,且锁定统一个资源。
一般来说,商业项目中,不考虑业务逻辑上的脏读问题,只考虑数值的脏读问题。但在航空、金融等领域要求较高。
private double d = 0.0;
public synchronized void m1(double d){
try {
// 相当于复杂的业务逻辑代码。
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.d = d;
}
public double m2(){
return this.d;
}
public static void main(String[] args) {
final Test_05 t = new Test_05();
new Thread(new Runnable() {
@Override
public void run() {
t.m1(100);
}
}).start();
System.out.println(t.m2());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.m2());
}
1.1.6 锁重入
可以看到在m1()睡眠期间,m2()执行结束。
锁重入:同一个线程,多次调用同步代码,锁定同一个锁对象,可重入。
synchronized void m1(){ // 锁this
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end");
}
synchronized void m2(){ // 锁this
System.out.println("m2 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2 end");
}
public static void main(String[] args) {
new Test_06().m1();
}
1.1.7 子类同步方法覆盖父类同步方法
可以看到子类同步方法end之前,所调用的父类同步方法执行结束。
子类同步方法覆盖父类同步方法。可以指定调用父类的同步方法,相当于锁重入。
public class Test_07 {
synchronized void m(){
System.out.println("Super Class m start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Super Class m end");
}
public static void main(String[] args) {
new Sub_Test_07().m();
}
}
class Sub_Test_07 extends Test_07{
synchronized void m(){
System.out.println("Sub Class m start");
super.m();
System.out.println("Sub Class m end");
}
}
1.1.8 锁与异常
当同步方法中发生异常的时候,自动释放锁资源。不会影响其他线程的执行
在实际编程中应捕获异常进行回滚操作。
int i = 0;
synchronized void m(){
System.out.println(Thread.currentThread().getName() + " - start");
while(true){
i++;
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(i == 5){
i = 1/0;
}
}
}
public static void main(String[] args) {
final Test_08 t = new Test_08();
new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}, "t2").start();
}
1.2 同步代码块
同步代码块的同步粒度更加细致, 是商业开发中推荐的编程方式。 可以定位到具体的同
步位置,而不是简单的将方法整体实现同步逻辑。在效率上,相对更高。
synchronized(o) 锁定的对象o是临界资源对象。
synchronized(this)锁定当前对象。
public void testSync1(){
synchronized(this){
代码...
}
}
1.2.1 锁定临界对象
同步代码块在执行时,是锁定 object 对象。当多个线程调用同一个方法时,锁定对象不
变的情况下,需同步执行。
private int count = 0;
private Object o = new Object();
public void testSync1(){
synchronized(o){
System.out.println(Thread.currentThread().getName()
+ " count = " + count++);
}
}
1.2.2 锁定当前对象
private int count = 0;
public void testSync2(){
synchronized(this){
System.out.println(Thread.currentThread().getName()
+ " count = " + count++);
}
}
1.2.3 锁对象变更问题
同步代码一旦加锁后,那么会有一个临时的锁引用执行锁对象,和真实的引用无直接关联。
在锁未释放之前,修改锁对象引用,不会影响同步代码的执行。
void m(){
System.out.println(Thread.currentThread().getName() + " start");
synchronized (o) {
while(true){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + o);
}
}
}
public static void main(String[] args) {
final Test_13 t = new Test_13();
new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}, "thread1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}, "thread2");
t.o = new Object();
thread2.start();
}
在修改锁对象引用后,线程一虽然仍然锁定的o1,线程二锁定的是o2。但是在打印中两个线程均打印o2;原因:在修改锁对象引用后,堆中o的引用随即指向o2;打印变量o是通过当前对象Test_13.class内o的指向找到要打印的值。
1.2.4
在定义同步代码块时,避免使用常量对象作为锁对象。
可以看到m2()没有执行。
String s1 = "hello";
String s2 = "hello";
void m1(){
synchronized (s1) {
System.out.println("m1()");
while(true){
}
}
}
void m2(){
synchronized (s2) {
System.out.println("m2()");
while(true){
}
}
}
public static void main(String[] args) {
final Test_14 t = new Test_14();
new Thread(new Runnable() {
@Override
public void run() {
t.m1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.m2();
}
}).start();
}
1.3 锁底层实现
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。同步方
法 并不是由 monitor enter 和 monitor exit 指令来实现同步的, 而是由方法调用指令读取运
行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。
对象头:存储对象的 hashCode、锁信息或分代年龄或 GC 标志,类型指针指向对象的类
元数据,JVM 通过这个指针确定该对象是哪个类的实例等信息。
实例变量:存放类的属性数据信息,包括父类的属性信息
填充数据: 由于虚拟机要求对象起始地址必须是 8 字节的整数倍。 填充数据不是必须存
在的,仅仅是为了字节对齐
当在对象上加锁时,数据是记录在对象头中。当执行 synchronized 同步方法或同步代码
块时,会在对象头中记录锁标记,锁标记指向的是 monitor 对象(也称为管程或监视器锁)
的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有
存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生
成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
在 Java 虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的。
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,以及_Owner 标记。其中_WaitSet
是用于管理等待队列(wait)线程的, _EntryList 是用于管理锁池阻塞线程的, _Owner 标记用于
记录当前执行线程。线程状态图如下
当多线程并发访问 同一个同步代码 时,首先会进入_EntryList,当线程获取锁标记后,
monitor 中的_Owner 记录此线程,并在 monitor 中的计数器执行递增计算(+1) ,代表锁定,
其他线程在_EntryList 中继续阻塞。若执行线程调用 wait 方法,则 monitor 中的计数器执行
赋值为 0 计算,并将_Owner 标记赋值为 null,代表放弃锁,执行线程进如_WaitSet 中阻塞。
若执行线程调用 notify/notifyAll 方法,_WaitSet 中的线程被唤醒,进入_EntryList 中阻塞,等
待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor 中的_Owner
标记赋值为 null,且计数器赋值为 0 计算。
1.4 锁的种类
Java 中锁的种类大致分为偏向锁,自旋锁,轻量级锁,重量级锁。
锁的使用方式为:先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升
级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。
锁只能升级,不能降级
1.4.1 偏向锁
是一种编译解释锁。 如果代码中不可能出现多线程并发争抢同一个锁的时候, JVM 编译
代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。使用
锁标记的形式记录锁状态。 在 Monitor 中有变量 ACC_SYNCHRONIZED。 当变量值使用的时候,代表偏向锁锁定。可以避免锁的争抢和锁池状态的维护。提高效率。
1.4.2 轻量级锁
过渡锁。当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升
为轻量级锁。 也是使用标记 ACC_SYNCHRONIZED 标记记录的。 ACC_UNSYNCHRONIZED 标记记
录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。
两个线程也可能出现重量级锁。
1.4.3 重量级锁
上面1.3介绍的便是重量级锁
1.4.4 自旋锁
是一个过渡锁,是偏向锁和轻量级锁的过渡。
当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环,再次申请
锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更。
2. volatile
保证变量的线程可见性而不是原子性。在 CPU 计算过程中,会将计算过程需要的数据加载到 CPU 计算缓
存中,当 CPU 计算中断时,有可能刷新缓存,重新读取内存中的数据。在线程运行的过程
中, 如果某变量被其他线程修改, 可能造成数据不一致的情况, 从而导致结果错误。 而 volatile
修饰的变量是线程可见的, 当 JVM 解释 volatile 修饰的变量时, 会通知 CPU, 在计算过程中,
每次使用变量参与计算时,都会检查内存中的数据是否发生变化,而不是一直使用 CPU 缓
存中的数据,可以保证计算结果的正确。
volatile 只是通知底层计算时,CPU 检查内存数据,而不是让一个变量在多个线程中同
步。
从以下案例中可以看到加上volatile修饰后,打印出end。在程序运行时cpu将变量b放入cpu缓存中,如果不加volatile修饰,cpu默认读取cpu缓存中的变量b的值而不是最新的值,反之,每次cpu在使用该变量的时候都会检查内存中数据的有效性,保证最新的内存数据被使用。
/*volatile*/ boolean b = true;
void m(){
System.out.println("start");
while(b){}
System.out.println("end");
}
public static void main(String[] args) {
final Test_09 t = new Test_09();
new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t.b = false;
}
3. wait¬ify
sleep()和wait()函数都可以挂起当前线程,使线程休眠,但实现方式不一样,如下:
1.首先sleep()是Thread类的方法静态方法,需要通过Thread类调用,Thread.sleep()。而wait()和notify()是Object类中的实例方法,因为java所有类都继承于object类,所有类中都可以使用。
2.wait(),和notify()必须用在synchronized代码块中调用,否则会抛出异常(因为wait()需要释放对象锁,如果不在synchronized代码块中不能保证拥有对象锁)。
3.当在synchronized代码块中使用sleep(),线程会被挂起,但不会释放对象锁,所以如果有其他线程等待执行该synchronized代码块,一直会被阻塞,等待该线程被唤醒释放对象锁。
4.当在synchronized代码块中使用wait(),线程会被挂起,需要notify()唤醒,但该线程会释放对象锁,所以其他线程可以执行该synchronized代码块。
4. AtomicXxx
原子类型。
在 concurrent.atomic 包中定义了若干原子类型, 这些类型中的每个方法都是保证了原子
操作的。多线程并发访问原子类型对象中的方法,不会出现数据错误。在多线程开发中,如
果某数据需要多个线程同时操作,且要求计算原子性,可以考虑使用原子类型对象。
注意:原子类型中的方法 是保证了原子操作,但多个方法之间是没有原子性的。如:
AtomicInteger i = new AtomicInteger(0);
if(i.get() != 5) i.incrementAndGet();
在上述代码中, get 方法和 incrementAndGet 方法都是原子操作,但复合使用时,无法
保证原子性,仍旧可能出现数据错误。
AtomicInteger count = new AtomicInteger(0);
void m(){
for(int i = 0; i < 10000; i++){
count.incrementAndGet();
}
}
public static void main(String[] args) {
final Test_11 t = new Test_11();
List<Thread> threads = new ArrayList<>();
for(int i = 0; i < 10; i++){
threads.add(new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}));
}
for(Thread thread : threads){
thread.start();
}
for(Thread thread : threads){
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(t.count.intValue());
}
5. CountDownLatch 门闩
门闩是 concurrent 包中定义的一个类型,是用于多线程通讯的一个辅助类型。
门闩相当于在一个门上加多个锁,当线程调用 await 方法时,会检查门闩数量,如果门
闩数量大于 0,线程会阻塞等待。当线程调用 countDown 时,会递减门闩的数量,当门闩数
量为 0 时,await 阻塞线程可执行。可以和锁混合使用,或替代锁的功能。优点:效率高。
CountDownLatch latch = new CountDownLatch(5);//定义5把锁
void m1(){
try {
latch.await();// 等待门闩开放。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m1() method");
}
void m2(){
for(int i = 0; i < 10; i++){
if(latch.getCount() != 0){
latch.countDown(); // 每次减掉门闩上的一把锁。
System.out.println("latch count : " + latch.getCount());
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Test_15 t = new Test_15();
new Thread(new Runnable() {
@Override
public void run() {
t.m1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.m2();
}
}).start();
}
6. 锁的重入
在 Java 中,同步锁是可以重入的。只有同一线程调用同步方法或执行同步代码块,对
同一个对象加锁时才可重入。
当线程持有锁时,会在 monitor 的计数器中执行递增计算,若当前线程调用其他同步代
码,且同步代码的锁对象相同时,monitor 中的计数器继续递增。每个同步代码执行结束,
monitor 中的计数器都会递减,直至所有同步代码执行结束,monitor 中的计数器为 0 时,释
放锁标记,_Owner 标记赋值为 null。
7. ReentrantLock
重入锁,建议应用的同步方式。相对效率比 synchronized 高。量级较轻。
synchronized 在 JDK1.5 版本开始, 尝试优化。 到 JDK1.7 版本后, 优化效率已经非常好了。
在绝对效率上,不比 reentrantLock 差多少。
使用重入锁, 必须 必须必须 手工释放锁标记。 一般都是在 finally 代码块中定义释放锁标
记的 unlock 方法。
7.1 尝试锁
Lock lock = new ReentrantLock();
void m1(){
try{
lock.lock();
for(int i = 0; i < 10; i++){
TimeUnit.SECONDS.sleep(1);
System.out.println("m1() method " + i);
}
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
void m2(){
boolean isLocked = false;
try{
// 尝试锁, 如果有锁,无法获取锁标记,返回false。
// 如果获取锁标记,返回true
// isLocked = lock.tryLock();
// 5秒内获取到锁标记返true,反之false
isLocked = lock.tryLock(5, TimeUnit.SECONDS);
if(isLocked){
System.out.println("m2() method synchronized");
}else{
System.out.println("m2() method unsynchronized");
}
}catch(Exception e){
e.printStackTrace();
}finally{
if(isLocked){
// 尝试锁在解除锁标记的时候,一定要判断是否获取到锁标记。
// 如果当前线程没有获取到锁标记,会抛出异常。
lock.unlock();
}
}
}
public static void main(String[] args) {
final Test_02 t = new Test_02();
new Thread(new Runnable() {
@Override
public void run() {
t.m1();
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
t.m2();
}
}).start();
}
公平锁会记录等待时长。t1 获取锁,t2等待 3 秒,t3等待 5 秒,当t1 执行结束,t3 有限获取锁
queue
公平锁图解
多次执行会出现某个线程连续获得锁的情况,初步认定为线程延迟了,即代码执行完后,线程并没有及时退出。
import java.util.concurrent.locks.ReentrantLock;
public class LockFairTest implements Runnable{
private static ReentrantLock lock=new ReentrantLock(true);
public void run() {
while(true){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+" get lock");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
LockFairTest lft=new LockFairTest();
Thread th1=new Thread(lft);
Thread th2=new Thread(lft);
th1.start();
th2.start();
}
}
查看线程状态,获取公平锁下出现一个线程连续获取锁的原因:
public class Test_04 {
public static void main(String[] args) {
TestReentrantlock t = new TestReentrantlock();
//TestSync t = new TestSync();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
class TestReentrantlock extends Thread{
AtomicInteger threadNum = new AtomicInteger(0);
// 定义一个公平锁
private static ReentrantLock lock = new ReentrantLock(true);
public void run(){
// for(int i = 0; i < 50; i++){
while(true){
lock.lock();
try{
getTrs();
System.out.println(Thread.currentThread().getName() + " get lock" );
if ("Thread-2".equals(Thread.currentThread().getName())) {
if (threadNum.get()==2) {
System.out.println(" 22 ==================== ");
System.exit(0);
}
threadNum.set(2);
}
if ("Thread-1".equals(Thread.currentThread().getName())) {
if (threadNum.get()==1) {
System.out.println(" 11 ==================== ");
System.exit(0);
}
threadNum.set(1);
}
}finally{
lock.unlock();
}
}
}
public void getTrs(){
Map<Thread, StackTraceElement[]> allThread = Thread.getAllStackTraces();
List<Object> ss = allThread.keySet().stream().collect(Collectors.toList());
for(Object b:ss){//打印出所有的线程名称
try {
Thread a=(Thread)b;
if ("Thread-2".equals(a.getName())) {
System.out.println("=================Thread-2 status : "+a.getState());
}
if ("Thread-1".equals(a.getName())) {
System.out.println("------------------------Thread-1 status : "+a.getState());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
运行结果:
8. 练习
1.分别使用volatile、wait/notify、CountDownLatch,在多线程并发情况下实现对共享变量对操作。
2. 生产者/消费者
- 案例待补充
9. ThreadLocal
ThreadLocal.set(value) -> map.put(Thread.getCurrentThread(), value);
ThreadLocal.get() -> map.get(Thread.getCurrentThread());
内存问题 : 在并发量高的时候,可能有内存溢出。
使用ThreadLocal的时候,一定注意回收资源问题,每个线程结束之前,将当前线程保存的线程变量一定要删除 。
- ThreadLocal.remove();
内存泄露问题和注意事项
ThreadLocal源码分析:(一)set(T value)方法
可以看到不同线程都保持单独都一份变量副本,每个线程对自己变量对修改不影响其它线程变量的值。
volatile static String name = "zhangsan";
static ThreadLocal<String> tl = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
System.out.println(tl.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
name = "lisi";
tl.set("wangwu");
}
}).start();
}