synchronized用于实现同步处理,保证共享数据的安全性
数据有安全性问题的原因:1、共享数据 2、修改数据
synchronized相对于volatile是重量级的线程安全的方法,可以保证3大特性:原子性、可见性、有序性。可以将并发操作转换为串型执行
- 用于静态方法,锁对象为当前类
public static synchronized void pp(){}
- 用于非静态方法,锁对象为当前类的对象
public synchronized void pp(){}
- 用于代码块,锁对象为指定的对象
synchronized(obj){}
用法1:同步代码块
public class A {
public static void main(String[] args) {
for(int i=0;i<3;i++) {
Thread t1=new MyThread("第"+(i+1)+"个售票窗口");
t1.start();
}
}
}
class MyThread extends Thread {
private String name;
private static int counter = 20;
private final static String LOCK="lock1";
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
while (counter > 0) {
try {
sleep(100);// 模拟售票过程,加剧出错的可能性
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK) {
if (counter > 0) {
System.out.println(this.name + "售出第" + counter + "号票");
// synchronized (LOCK) {
counter--;
// }
} else {
System.out.println("票已经售尽!");
}
}
}
}
}
synchronized用法2:同步方法 在方法上添加同步关键字,当前的锁对象为当前对象no—对象锁
定义4个线程对同一个num进行加减计算,2个线程各执行50次加,2个线程各执行50次减
public class A2 {
public static void main(String[] args) throws Exception {
NumOper no = new NumOper(100);
Thread[] ts = new Thread[4];
for (int i = 0; i < 2; i++) {
ts[i * 2] = new Thread(() -> {
for (int k = 0; k < 50; k++)
no.add();
});
ts[i * 2].start();
ts[i * 2 + 1] = new Thread(() -> {
for (int k = 0; k < 50; k++)
no.sub();
});
ts[i * 2 + 1].start();
}
for (Thread tmp : ts)
tmp.join();
System.out.println("Main:" + no.getNum());
}
}
/*
* 以new出来的NumOper对象充当锁,当前对象内的所有synchronized方法在不同线程调用时互斥,
* 但是可以直接访问非synchronized方法。注意synchronized允许持有锁的线程重入
*/
class NumOper {
private int num;
public NumOper(int num) { //synchronized不能添加在构造器上
this.num = num;
}
public synchronized void add() {
System.out.println(Thread.currentThread() + "....add...begin:" + this.num);
this.num++;
sub();
System.out.println(Thread.currentThread() + "....add...end" + this.num);
}
public synchronized void sub() {
System.out.println(Thread.currentThread() + "....sub...begin" + this.num);
this.num--;
System.out.println(Thread.currentThread() + "....sub...end" + this.num);
}
public int getNum() {
return this.num;
}
}
synchronized用法3:同步静态方法,以当前类Class对象作为锁—类锁
针对一个类一般只会存储一个
//统计指定类的创建次数
public class A3 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int k = 0; k < 10; k++)
new S3();
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(S3.getCounter());
}
}
class S3 {
private static int counter = 0;
public S3() {
add();
}
private synchronized static void add() {
System.out.println(Thread.currentThread() + "开始创建操作" + counter);
counter++;
eee();
System.out.println(Thread.currentThread() + "完成创建操作" + counter);
}
public synchronized static void eee() {
}
public static int getCounter() {
return counter;
}
}
使用类锁,所以不管new了多少个对象,都可以得到互斥的效果
使用的是对象锁,所以只能new一个对象,才可以得到互斥的效果。如果创建多个则不能达到互斥的目的
引入锁机制以解决线程安全问题:
- 悲观锁:当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程 在操作共享数据,其他线程必须等到该线程处理完数据后再进行
- 乐观锁:CAS compare and set
synchronized总结
synchronized同步关键字,用于代码同步处理,解决线程安全问题
- synchronized同步方法 以当前对象充当锁
public synchronized void pp(){}
- synchronized同步静态方法 以当前类Class充当锁
public synchronized static void pp(){}
- synchronized同步代码块 自定义对象充当锁
synchronized(obj){}
synchronized原理
在添加synchronized关键字后就可以保证在一个时刻上只有一个线程在调用某个方法或 者代码块,不会出现并发的情形,达到排队执行的效果。
在Java中synchronized可保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时还应该注意到 synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能),这点确实也是很重要的。
在JDK1.6之前一般不建议使用synchronized,因为相比较Lock接口而言,是重量级的
jdk6之前是重量级锁,JDK6开始优化锁的状态总共有四种,无锁状态(使用乐观锁CAS, 没有synchronized)、偏向锁、轻量级锁和重量级锁。锁状态的改变是根据竞争激烈程度进行的,在几乎无竞争的条件下,会使用偏向锁,在轻度竞争的条件下,会由偏向锁升级为轻量级锁, 在重度竞争的情况下,会升级到重量级锁。 随着锁的竞争,锁可 以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只 能从低到高升级,不会出现锁的降级
对象在内存中存储时可以分为对象头、实例数据和对齐字节三部分。对象头数据一般包含标识字mark word和类型指针klass pointer;实例数据就是具体对象的成员数据,一般按照4B为的单位进行数据存储;最后的对齐字节用于将对象存储的数据凑够8字节的整数倍。
-
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对 象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间
内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间
Mark Word里存储的数据会随着锁标志位的变化而变化 -
Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
-
synchronized用的锁是存在Java对象头里的,存在锁对象的对象头的Mark Word中
锁对比
锁 | 描述 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
偏向锁 | 线程在大多数情况下并不存在竞争条件,使用同步会消耗性能,而偏向锁是对锁的优化,可以消除同步,提升性能。当一个线程获得锁,会将对象头的锁标志位设为01,进入偏向模式。偏向锁可以在让一个线程一直持有锁,在其他线程需要竞争锁的时候,再释放锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 当线程A获得偏向锁后,线程B进入竞争状态,需要获得线程A持有的锁,那么线程A撤销偏向锁,进入无锁状态。线程A和线程B交替进入临界区,偏向锁无法满足,膨胀到轻量级锁,锁标志位设为00 | 竞争的线程不会阻塞,提高了程序的相应速度 | 如果始终得不到所竞争的线程,使用自旋会消耗CPU | 追求相应速度,同步块执行速度非常块 |
重量级锁 | 当多线程交替进入临界区,轻量级锁hold得住。但如果多个线程同时进入临界区,hold不住了,膨胀到重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
- 会同时被多个线程访问的资源,就是竞争资源(临界资源),也称为竞争条件。对于多线程共享的资源(临界资源)必须进行同步,以避免一个线程的改动被另一个线程所覆盖
同步代码块的同步方法的实现区别
- 同步语句块使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令指向同步代码块的结束位置
- 方法的同步是隐式的,也就是说 synchronized 修饰方法的底层无需使用字节码来控制。
synchronized修饰的方法并没有使用 monitorenter 和 monitorexit 指令,取得代之是ACC_SYNCHRONIZED 标识,该标识指明了此方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这就是 synchronized 锁在同步代码块上和同步方法上的实现差别。
Monitor 对象
任何对象都关联了一个管程,管程就是控制对象并发访问的一种机制。
可以理解 synchronized 就是 Java 中对管程的实现。管程提供了一
种排他访问机制,这种机制也就是互斥。互斥保证了在每个时间点上,
最多只有一个线程会执行同步方法。所以理解了 Monitor 对象其实就是
使用管程控制同步访问的一种对象。
monitor对象是monitor机制的核心,它本质上是jvm用c语言定义的一个
数据类型。对应的数据结构保存了线程同步所需的信息,比如保存了被阻
塞的线程的列表,还维护了一个基于mutex的锁,monitor的线程互斥就是
通过mutex互斥锁实现的。
线程同步synchronized 小结
1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏【多线程访问和修改】。
2、线程同步方法是通过锁(监视者Mintor)来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的同步方法(可以访问静态同步方法)。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对需要具有原子操作的步骤做出分析,并保证原子操作期间别的线程无法访问竞争资源(加锁处理)。StringBuffer线程安全,StringBuilder线程不安全
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使。但是,一旦程序发生死锁,程序将死掉。
常见问题
1、为什么调用 Object 的 wait/notify/notifyAll 方法,需要加 synchronized 锁。
因为这3个方法都会操作锁对象,所以需要先获取锁对象,而synchronized 锁可以让 我们获取到锁对象
2、synchronize 底层维护了几个列表存放被阻塞的线程
synchronized 底层对应的 JVM 模型为objectMonitor,使用了3个双向链表来存放被阻塞 的线程:_cxq(Contention queue)
、_EntryList(EntryList)
、_WaitSet(WaitSet)
。
-
当线程获取锁失败进入阻塞后,首先会被加入到_cxq链表,_cxq链表的节点会在某个 时刻被进一步转移到_EntryList链表。
-
当持有锁的线程释放锁后,_EntryList链表头结点的线程会被唤醒,该线程称为 successor(假定继承者),然后该线程会尝试抢占锁。当我们调用wait() 时,线程会被放入_WaitSet,直到调用了notify()/notifyAll()后,线程才 被重新放入_cxq或_EntryList,默认放入_cxq链表头部。
3、为什么释放锁时被唤醒的线程会称为“假定继承者”?被唤醒的线程一定能获取到锁吗?
因为被唤醒的线程并不是就一定获取到锁了,该线程仍然需要去竞争锁,而且可能会失败,所 以该线程并不是就一定会成为锁的“继承者”,而只是有机会成为,所以我们称它为假定的。 这也是 synchronized 为什么是非公平锁的一个原因。
4、synchronized 是公平锁还是非公平锁?
非公平锁。
5、synchronized 为什么是非公平锁?非公平体现在哪些地方?
synchronized 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计, 核心有以下几个点: 1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作: - 先将锁的持有者 owner 属性赋值为 null - 唤醒等待链表中的一个线程(假定继承者) 在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。 > > 2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的 ,也就是说你先进入链表,不代表你就会先被唤醒。
6、如果有多个线程都进入wait状态,那某个线程调用notify唤醒线程时是否按照进入wait的
顺序去唤醒?答案是否定的。 调用 wait 时,节点进入`_WaitSet`链表的尾部。调用 notify 时,根据不同的策略,节点 可能被移动到 cxq头部、cxq 尾部、EntryList 头部、EntryList 尾部等多种情况。所以, 唤醒的顺序并不一定是进入 wait 时的顺序。
7、notifyAll 是怎么实现全唤起的?
nofity 是获取 WaitSet 的头结点,执行唤起操作。
nofityAll 的流程,可以简单的理解为就是循环遍历WaitSet的所有节点,对每个节点执行
notify 操作。
8、JVM 做了哪些锁优化?
偏向锁、轻量级锁、自旋锁、自适应自旋、锁消除、锁粗化。
9、为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?
> 重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。 > >
这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。而在很多情况下,
可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。
10、偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?
> 偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次 CAS 操作修改 markword
,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的 CAS 操作。 > > 如果确定同步代码块会被多个线程访问或者竞争较大,可以通过
-XX:-UseBiasedLocking 参数关闭偏向锁。