一、Synchronized的作用
作用:能够保证在同一时刻
最多只有一个线程
执行该代码,以达到保证并发安全的效果
public class DisappearRequest implements Runnable{
static DisappearRequest dr = new DisappearRequest();
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(dr);
Thread t2 = new Thread(dr);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
// 结果count小于20000(线程不安全)
二、Synchronized的两个用法
1. 对象锁:包括同步代码块锁
(自己指定锁对象)和方法锁
(默认锁对象为this当前实例对象)
1.1 代码块形式:手动指定锁对象
public class SynchronizedObjectCodeBlock implements Runnable {
static SynchronizedObjectCodeBlock instance =
new SynchronizedObjectCodeBlock();
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
@Override
public void run() {
// 两个线程串行操作
synchronized(this){
System.out.println("我是对象锁的代码块形式。我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
}
public class SynchronizedObjectCodeBlock implements Runnable {
static SynchronizedObjectCodeBlock instance =
new SynchronizedObjectCodeBlock();
Object lock1 = new Object();
Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
@Override
public void run() {
synchronized(lock1){
System.out.println("我是对象锁的代码块形式。我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
synchronized(lock2){
System.out.println("我是对象锁的代码块形式。我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
}
// CountDownLatch、信号量解决线程同步问题。
1.2 方法锁形式:synchronized修饰普通方法,锁对象默认为this
普通方法锁
public class SynchronizedMethodLock implements Runnable{
static SynchronizedMethodLock instance =
new SynchronizedMethodLock();
@Override
public void run() {
sync();
}
// 普通方法锁(不能是静态方法。锁对象默认是this)
public synchronized void sync(){
System.out.println("我是普通方法锁形式。我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
}
2. 类锁:指synchronized修饰静态方法或指定锁为Class对象。Java类可能有很多个对象,但只有一个Class对象。所谓的类锁,不过是Class对象的锁而已。
用法和效果:类锁只能在同一时刻被一个对象拥有,其他对象会被阻塞
对象锁如果不同的实例创建出来的,互相锁是不受影响的,你可以运行我也可以运行,并行运行,但是类锁只有一个可以运行。
2.1. synchronized加在static方法上。
场景:如果需要在全局情况下同步该方法,而不是一个小范围层面,则应该用这种形式去做同步保护。
public class SynchronizedMethodLock implements Runnable{
// 创建两个实例对象
static SynchronizedMethodLock instance1 =
new SynchronizedMethodLock();
static SynchronizedMethodLock instance2 =
new SynchronizedMethodLock();
@Override
public void run() {
sync();
}
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
public static synchronized void sync(){
System.out.println("我是类锁的第一种形式:static形式。我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
}
2.2. synchronized(*.class)代码块
public class SynchronizedMethodLock implements Runnable{
// 创建两个实例对象
static SynchronizedMethodLock instance1 =
new SynchronizedMethodLock();
static SynchronizedMethodLock instance2 =
new SynchronizedMethodLock();
@Override
public void run() {
sync();
}
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
public void sync(){
// 如果是this的话则并行执行,Class对象则串行执行
synchronized(SynchronizedMethodLock.class){
System.out.println("我是类锁的第二种形式:synchronized(*.class)形式。我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
}
三、线程不安全导致请求丢失问题解决
场景:前面【一、Synchronized的作用
】中的demo计数场景。
如下四种方式解决(结果均为20000):
3.1
@Override
public synchronized void run() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
3.2
@Override
public void run() {
synchronized (this){
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
3.3
@Override
public void run() {
synchronized(DisappearRequest.class){
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
3.4
@Override
public void run() {
add();
}
public static synchronized void add(){
for (int i = 0; i < 10000; i++) {
count++;
}
}
四、多线程访问同步方法的7中情况
4.1、 两个线程同时访问一个对象的同步方法
两个线程争抢的是同一把锁,线程间相互等待,只能有一个线程持有该锁
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
4.2、 两个线程访问的是两个对象的同步方法
创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行,这两个实例真正采用的锁对象不是同一个,所以不会被干扰。
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
public void sync(){
// 如果是this的话则并行执行,指向的是不同的实例对象,若为Class对象则串行执行
synchronized(this){
// TO DO...
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
4.3、 两个线程访问的synchronized的静态方法
如果两个线程访问的是同一个对象的同步方法则串行执行,如果访问的是不同对象的同步方法,若该方法是非静态static方法则并行执行,否则两个线程访问的锁对象为同一把锁,串行执行。
public static synchronized void sync(){
// TO DO...
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
4.4 同时访问同步方法合肥同步方法
非同步方法不受到影响
4.5 访问同一个对象的不同的普通同步方法
同一个对象,两个同步方法拿到的this是一样的,同一把锁,所以串行执行
4.6 同时访问静态synchronized和非静态synchronized方法
两个线程指定的锁对象不是同一把锁,所以锁之间不冲突,并行执行
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
public static synchronized void sync(){
// 如果是this的话则并行执行,Class对象则串行执行
System.out.println("我是静态方法:synchronized(*.class)形式。我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
public synchronized void sync1(){
// 如果是this的话则并行执行,Class对象则串行执行
System.out.println("我是非静态方法:synchronized(*.class)形式。我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance1);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
4.7 方法抛异常后,是否会释放锁
抛出异常之后jvm会释放锁,后面的线程会进入同步方法。
// 创建两个实例对象
static SynchronizedMethodLock instance1 =
new SynchronizedMethodLock();
static SynchronizedMethodLock instance2 =
new SynchronizedMethodLock();
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
sync();
}else{
sync1();
}
}
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
public synchronized void sync(){
// 如果是this的话则并行执行,Class对象则串行执行
System.out.println("我是方法1:我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException();
// System.out.println(Thread.currentThread().getName() + "运行结束");
}
// 创建两个实例对象,如果不是static方法的话,则并行操作,否则串行执行。
public synchronized void sync1(){
// 如果是this的话则并行执行,Class对象则串行执行
System.out.println("我是方法2:我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance1);
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
}
System.out.println("finished");
}
五、synchronized的性质
【5.1 可重入】:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁好处:避免死锁,提升封装性
比如:现在有两个均被synchronized修饰的方法f1和f2,此时线程A执行到了f1,并且获得了这把锁,由于方法二f2也是synchronized修饰(也就是说要执行f2必须拿到f2的锁),如果synchronized不具备可重入性,此时线程A只是拿到了f1的锁,没有拿到f2的锁,所以线程A即想拿到f2的锁又不释放f1锁,那么就会造成永远等待,即死锁,所以synchronized是具有可重入性。
【可重入粒度如下】
5.1.1 证明同一个方法是可重入的(递归)5.1.2 证明可重入不要求是同一个方法
public synchronized void method1(){
System.out.println("我是method1");
method2();
}
public synchronized void method2(){
System.out.println("我是method2");
}
5.1.3 证明同可重入不要求是同一个类中的
public class SyncSuperClass{
public synchronized void doSomething(){
System.out.println("我是父类方法");
}
}
class TestClass extends SyncSuperClass{
public synchronized void doSomething(){
System.out.println("我是子类方法");
super.doSomething();
}
}
【5.2 不可中断性】
一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我只能永远等下去。
Lock类
:相比之下,Lock类,可以拥有中断的能力,第一点,如果我觉得我等的时间太长了,有权中断现在已经获取到锁的线程的执行;第二点,如果我觉得等待时间太长了不想等了,可以退出。
Lock lock = new ReentrantLock();
// 下面这两种形式的锁是等价的
public synchronized void method1(){
System.out.println("我是Synchronized形式的锁");
}
public void method2(){
lock.lock();
try{
System.out.println("我是Lock形式的锁");
}finally{
lock.unlock();
}
}
六、synchronized的缺陷
【6.1 效率低】
锁的释放情况少,试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程。
6.1.1、 当一个线程获得了对应的sync锁的时候,其他线程只能等待我释放之后才能获取该锁。
6.1.2、 只有两种情况才释放锁:1.执行完了这段代码,2.发生异常自动释放锁
6.1.3、 不能中断,但是Lock是有中断能力的
【6.2 不够灵活(如:读写锁比较灵活:读的时候不加锁,写才加锁)】
加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
【6.3 无法知道是否成功获取到锁】
七、Lock锁常用方法
Lock lock = new ReentrantLock(); // 非公平锁
// Lock lock = new ReentrantLock(true); // 公平锁
// Lock lock = new ReentrantReadWriteLock();
lock.lock();
lock.unlock();
lock.tryLock(); // 获取锁
lock.tryLock(10, TimeUnit.SECONDS);
八、常见面试题
8.1 使用synchroinzed注意点:锁对象不能为空、作用域不宜过大、避免死锁
注:一个对象作为锁对象,这个对象必须是被new过的,或者是被其他方法创建好的,而不是一个空对象,因为锁的信息保存在对象头中的。8.2 如何选择Lock和synchronized关键字
尽量使用concurrent包下的CountDownLatch或者atomic包,或者信号量
九、思考
9.1 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个县城?
9.2 synchronized使得同时只能有一个线程可以执行,性能较差,有什么办法可以提升性能?
9.3 我想更灵活的控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?
9.4 什么是锁的升级、降级?什么事JVM里的偏斜所、轻量级锁。重量级锁?