synchronized总结

线程安全概念:当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。

如下的例子就是一个线程不安全的例子:我们预期依次输出4,3,2,1,0,结果如下:
t1 count = 2
t3 count = 2
t2 count = 2
t4 count = 1
t5 count = 0

public class MyThread extends Thread{
private static int count = 5 ;
//synchronized加锁
public void run(){
count--;
System.out.println(this.currentThread().getName() + " count = "+ count);
}
public static void main(String[] args) {
/**
* 分析:当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排对是按照CPU分配的先后顺序而定的)
*/
new Thread(new MyThread(),"t1").start();
new Thread(new MyThread(),"t2").start();
new Thread(new MyThread(),"t3").start();
new Thread(new MyThread(),"t4").start();
new Thread(new MyThread(),"t5").start();
}
}

线程不安全的原因:
java的内存机制有关,下面简要介绍下jvm的内存。
学过C语言的朋友都知道C编译器在划分内存区域的时候经常将管理的区域划分为数据段和代码段,数据段包括堆、栈以及静态数据区。那么在Java语言当中,内存又是如何划分的呢?
途中紫色部分是线程共享的内存区域,绿色部分是线程独享的内存区域。
方法区:
方法区上的数据大部分都是在程序编译完就已经确认好的,存放的是static类型的数据,或者字符串常量。
。。。。
分析:当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排对是按照CPU分配的先后顺序而定的),count变量是每个线程所共享的。。。所以,运行结果不符合我们的预期。

改造后的程序。
public class MyThread extends Thread{
private static int count = 5 ;
//synchronized加锁
public void run(){
synchronized (MyThread.class) {
count--;
System.out.println(this.currentThread().getName() + " count = "+ count);
}
}
public static void main(String[] args) {
/**
* 分析:当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排对是按照CPU分配的先后顺序而定的)
*/
new Thread(new MyThread(),"t1").start();
new Thread(new MyThread(),"t2").start();
new Thread(new MyThread(),"t3").start();
new Thread(new MyThread(),"t4").start();
new Thread(new MyThread(),"t5").start();
}
}

运行结果如下:
t1 count = 4
t4 count = 3
t5 count = 2
t3 count = 1
t2 count = 0

说明:
(1)synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"。
(2)一个线程想要执行synchronized修饰的方法里的代码:
1 尝试获得锁
2 如果拿到锁,执行synchronized代码体内容;拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止, 而且是多个线程同时去竞争这把锁。
(3)java中的锁锁的是什么?这个问题很关键,搞不懂的话,就不能正确的使用锁和synchronized。
经过查找资料发现,原来在java的对象头里面又个字段来标示该对象有没有被上锁。什么是对象头呢?
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。
对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。
这里要特别关注的是 锁标志位,    锁标志位与是否偏向锁对应到唯一的锁状态。
所以锁的状态保存在对象头中。

重要结论1:
* 关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,
* 所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock),
* 在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)。
下面的例子说明了这个结论,注意观察下下面代码的输出。
public class MultiThread {

private int num = 0;
/** static */
public synchronized void printNum(String tag){
try {
if(tag.equals("a")){
num = 100;
System.out.println("tag a, set num over!");
Thread.sleep(1000);
} else {
num = 200;
System.out.println("tag b, set num over!");
}
System.out.println("tag " + tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//注意观察run方法输出顺序
public static void main(String[] args) {
//俩个不同的对象
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}

运行结果如下:
tag a, set num over!
tag b, set num over!
tag b, num = 200
tag a, num = 100

运行结果是交叉出现的,说明两个线程之间的锁,互不影响,不同对象之间的数据是隔离的。

重要结论2:
* t1线程先持有object对象的Lock锁,t2线程可以以异步的方式调用对象中的非synchronized修饰的方法
* t1线程先持有object对象的Lock锁,t2线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步

重要结论3:
synchronized 是可重入的,t1线程获得了这个对象的锁,可以访问这个类的其他加锁的方法,如下:
ps,父类继承过来的方法也可以。

public class SyncDubbo1 {

public synchronized void method1(){
System.out.println("method1..");
method2();
}
public synchronized void method2(){
System.out.println("method2..");
method3();
}
public synchronized void method3(){
System.out.println("method3..");
}
public static void main(String[] args) {
final SyncDubbo1 sd = new SyncDubbo1();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sd.method1();
}
});
t1.start();
}
}


重要结论4:
一个线程如果不捕获异常,当异常发生时,这个线程或挂掉。
public class SyncException {

private int i = 0;
public synchronized void operation(){
while(true){
try {
i++;
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " , i = " + i);
if(i == 20){
//Integer.parseInt("a");
throw new RuntimeException();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final SyncException se = new SyncException();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
se.operation();
}
},"t1");
t1.start();
}
}

重要结论5: 避免换锁的情况发生。如下面的例子。在synchronized的方法中锁定的对象发生了改变,会发生锁不住的情况,这也验证了synchronized锁定的是对象。
public class ChangeLock {

private String lock = "lock";
private void method(){
synchronized (lock) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
lock = "change lock";
Thread.sleep(2000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ChangeLock changeLock = new ChangeLock();
new Thread(()->changeLock.method(),"t1").start();
new Thread(()->changeLock.method(),"t2").start();
}
}
当前线程 : t1开始
当前线程 : t2开始
当前线程 : t1结束
当前线程 : t2结束

重要结论6: 同一对象的属性修改不会影响锁的情况,锁是在对象头上标记的。
重要结论7:避免死锁
重要结论8:锁定对象,锁定方法。锁定方法分两种情况。
一:锁定的事静态方法:锁定的是这个对象的原数据。 XXXX.class对象
二:普通方法:锁定的是this
重要结论9:使用synchronized代码块减小锁的粒度,提高性能

重要结论10:synchronized代码块对字符串的锁,注意String常量池的缓存功能。尽量不要锁定string的对象。

public class StringLock {

public void method() {
String a = "字符串常量";
synchronized (a) {
try {
while(true){
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
Thread.sleep(1000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(()->new StringLock().method(), "t1").start();
new Thread(()->new StringLock().method(), "t2").start();
}
}

当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始

public class StringLock {

public void method() {
String a = String.valueOf("字符串常量");
synchronized (a) {
try {
while(true){
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
Thread.sleep(1000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(()->new StringLock().method(), "t1").start();
new Thread(()->new StringLock().method(), "t2").start();
}
}

当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t1结束
当前线程 : t1开始

public class StringLock {

public void method() {
String a = new String("字符串常量");
synchronized (a) {
try {
while(true){
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
Thread.sleep(1000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(()->new StringLock().method(), "t1").start();
new Thread(()->new StringLock().method(), "t2").start();
}
}

当前线程 : t1开始
当前线程 : t2开始
当前线程 : t2结束
当前线程 : t2开始
当前线程 : t1结束
当前线程 : t1开始
当前线程 : t2结束
当前线程 : t2开始
当前线程 : t1结束
当前线程 : t1开始

最后一个重要结论:多个addAndGet在一个方法内是非原子性的,需要加synchronized进行修饰,保证4个addAndGet整体原子性。



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值