文章目录

  • synchronized锁定的资源
  • 锁定对象改变
  • 锁定对象为字符串常量
  • 减小锁的颗粒度
  • 脏读问题
  • 支持重入
  • 重入1
  • 重入2
  • synchronized 与 异常处理
  • synchronized的可见性问题
  • 原子性操作
  • 一道的面试题
  • 方式1:
  • 方式2:
  • 方式3:


synchronized锁定的资源

synchronized修饰的是方法或者代码块来实现同步,但其实锁定的资源其实是对象synchronized修饰于3种方式(静态方法、普通方法、方法块),其锁定的资源有2种(类对象、类的实例)加synchronized关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一

  1. synchronized修饰静态方法,锁定的资源是Class对象(类对象,Class class = x x x.Class)
    两种方式:
public class Demo {
    private static int count = 10;
    //synchronize关键字修饰静态方法锁定的是类对象
    //方式1
    public synchronized static void test(){
    //临界区
        count--;
    }
    //方式二
    public static void test2(){
        synchronized (Demo4.class){//这里不能替换成this
        //临界区
            count--;
        }
    }
}
  1. synchronized修饰普通方法,锁定的资源是类的实例(User o = new User())

三种方式:

  • 对象用new的方式
private int count = 10;
    private Demo reentryDemo = new Demo();
    public void test(){
        synchronized (object){
            //临界区
            count--;
        }
    }
  • 用this来代替,当前对象的实例
public void test() {
  synchronize (this) {}
}
  • 直接修饰在普通方法上
public synchronized void test() {}
  1. synchronized修饰代码块/方法块,其实实际上也是方法的一部分, 所以锁定的资源也是根据方法来。

锁定对象改变

  1. 如果锁定对象的属性发生改变,不会影响锁的使用。
  2. 如果锁定对象修改为另外一个对象,这种锁定对象的改变了,等于是一把新锁了。
    示例代码:
public class Demo {
    public Lock lock = new Lock();
    public void test () {
        synchronized (lock) {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("hello word");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Demo reentryDemo = new Demo();
        new Thread(reentryDemo::test,"t1").start();
        TimeUnit.MILLISECONDS.sleep(10);
//        reentryDemo.lock.i=20;
        reentryDemo.lock = new Lock();
        new Thread(reentryDemo::test,"t2").start();
    }
}
class Lock {
    public int i = 0;
}

锁定对象为字符串常量

不要以字符串常量作为锁定的对象,在jvm中两个相同的字符串都是指向常量池同一块内存。

所以虽然名字不同,但实际上还是同一把锁。

public String sLock1 = "lock";
    public String sLock2 = "lock";
    public void test1 () {
        synchronized (sLock1) {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("hello word");
            }
        }
    }
    public void test2() {
        synchronized (sLock2) {
            log.debug("test2 end");
        }
    }

另外一种情况,不属于并发的知识点了。这种就是2个对象了。

public String sLock1 = new String("lock");
public String sLock2 = new String("lock");

减小锁的颗粒度

通过减少同步的代码块,降低锁的颗粒度。同步的代码块越少越好,从而提高效率。

比如说在业务上只要保证count的同步,可以采用细颗粒度的锁,从而减少线程竞争的时间。

优化方案:就只需要对count++局部上锁,不用对整个方法上锁。

Bad:

int count = 0;

public synchronized void test(){
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    count ++;

    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

Good:局部上锁

public void test2(){
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    synchronized (this) {
        count ++;
    }

    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

脏读问题

一般对于set方法需要进行上锁来解决线程安全问题,但是get呢?因为get不会使对象产生线程安全问题,只是会出现脏读问题。

对于脏读的解决应该具体分析具体讨论,因为加锁其实会使代码效率变低。

实际业务当中应该看是否允许脏读,不允许的情况下对读方法也要加锁。

支持重入

重入1

Q:如果一个同步方法调用另一个同步方法,另一个同步能否被调用呢?

A:synchronized默认是支持重入的,所以能被调用成功。

Q:为何需要给另一个方法也上锁呢?

A:是保证另一个方法也会被单独调用的时候,是线程安全的。

重入2

Q:父类的同步方法,子类进行重写,重写方法也调用了父类的同步方法,能否被调用?

A:也是可以的。这种也是重入锁的应用之一。

code:

@Slf4j(topic = "console")
public class ReentryDemo {
    //一个同步方法调用另外一个同步方法,能否获得锁?
    public synchronized void test1() {
        log.debug("test1 start.........");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
      //调用另一个同步方法
        test2();
        log.debug("test1 end.........");
    }
    public synchronized void test2() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("test2 start & end.");
    }

    public static void main(String[] args) {
        Demo2 reentryDemo = new Demo2();
        reentryDemo.test1();
        new Thread(reentryDemo::test2).start();
    }
}
@Slf4j(topic = "console")
class Demo2 extends ReentryDemo {
  //重写父类的同步方法,并调用父类的同步方法
    @Override
    public synchronized void test1() {
        log.debug("sub test1 start.........");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        super.test1();
        log.debug("sub test1 end.........");
    }
}

synchronized 与 异常处理

当同步方法报异常的时:

  1. 如果没有对异常处理,锁会释放掉,线程不安全了。
  2. 如果对异常进行处理了,锁不会被释放。

所以在并发编程中,一定要对异常进行处理。

Code:

@Slf4j(topic = "console")
public class ExceptionDemo {

    Object o = new Object();

    int count = 0;

    void test() {
        //
        synchronized (o) {
            //t1进入并且启动
            log.debug("start......");
            //t1 会死循环 t1 是不会释放锁的
            while (true) {
                count++;
                log.debug(" count = {}", count);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //加5次之后 发生异常
                /**
                 * 代码出异常如果不处理则会释放锁
                 * 如果处理了则不会释放锁
                 */
                if (count == 5) {
                    try {
                        int i = 1 / 0;
                    } catch (Exception e) {

                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExceptionDemo demo = new ExceptionDemo();
        new Thread(() -> {
            demo.test();
        }, "t1").start();
        TimeUnit.MILLISECONDS.sleep(1);
        new Thread(() -> {
            demo.test();
        }, "t2").start();
    }
}

synchronized的可见性问题

多个线程共享一个变量时,会出现可见性问题。

当有线程修改变量的值后,其他线程看不到值的变化,这就是可见性问题。

@Slf4j(topic = "console")
public class Demo {
    boolean running = true;
    List<String> list = new ArrayList<>();
    /**
     * t1线程
     */
    public  void test(){
        log.debug("test start...");
        while (running){
        }
        log.debug("test end...");
    }
    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        new Thread(demo :: test,"t1").start();
        TimeUnit.SECONDS.sleep(2);
        demo.running = false;
    }
}

Q:上面的启动t1线程后,在主线程改变了running的值,t1线程会停止吗?

A:不会。t1线程一直在死循环。

原因:t1线程和主线程都共用了running变量,该变量会在t1保留一份副本在栈上,这样main修改了running,t1也不知道。这种可见性的问题实际上是指令重排导致的。running这个副本在t1线程中的形式如下(不是真的是这样,只是伪代码,表达大概意思),这样main吧running怎么修改都不会重新读到running了。

boolean flag =running;
  	if (flag) {
  		while(true){}
		}

避免这种现象发生的做法:

  1. 在while(running)里添加一些操作,让jvm不敢指令重排。
  2. volatile修饰变量running,让使用这个变量的线程都需要到线程去读取。

volatitle这个关键字在C语言常考的,不到寄存器中读取变量的备份,而是每次都要在内存中读取。避免

如果要深究的话参考R大的回答:https://hllvm-group.iteye.com/group/topic/34932

原子性操作

需求:

启动10个线程对count进行1w次++操作,应得结果是10w。

  1. 用volatile修饰count,因为volatile不能保证原子性,所以所得结果一定是小于10w的。
  2. 为了保证原子性可以用synchronized上锁,能保证结果准确性,但是上锁的效率较低。
  3. 利用原子类AtomicInteger。count++对于jvm来说需要执行多条指令,而count2.incrementAndGet();对于jvm来说是执行“一条指令”。

⚠️注意:atomic类连续调用不能构成成原子性

Code:

@Slf4j(topic = "console")
public class Demo {
    //volatile只能保证有序性和可见性,不能保证原子性
    volatile int count = 0;
    public void test(){
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }
    //可以通过上锁的方式保证有序性,可见性,和原子性
    int count1 = 0;
    public synchronized void test1(){
        for (int i = 0; i < 10000; i++) {
            count1++;
        }
    }
    //可以通过AtomicX变量来保证原子性
    AtomicInteger count2 = new AtomicInteger(0);
    public void test2(){
        for (int i = 0; i < 10000; i++) {
            count2.incrementAndGet();
        }
    }
    //如果我们只想执行到1000就不加了,这样能保证原子性吗,结果会是1k吗?
    //对原子类多次调用其实不能保证原子性的。
    // count2.get能保证原子性,count2.incrementAndGet也能保证原子性
    //但是他们2个不能保证原子性。
    public void test3(){
        for (int i = 0; i < 10000; i++) {
            if (count2.get()<1000) {
                count2.incrementAndGet();
            }
        }
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        List<Thread> threads = new ArrayList();
        //new 10个线程
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(demo::test2, "t-" + i));
        }
        //遍历这个10个线程  依次启动,都同时对count进行一万次++
        threads.forEach((o)->o.start());
        //等待10个线程执行完
        threads.forEach((o)->{
            try {
                o.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        /*
         结果是 小于等于10万的,因为不能保证原子性
         */
        log.debug(demo.count+"");
        /*
        通过上锁的方式效率会比较低
         */
        log.debug(demo.count1+"");
        /*
        使用AtomicX保证原子性
         */
        log.debug(demo.count2+"");
    }

}

一道的面试题

* 需求:
* 实现一个容器,提供两个方法,add,size
* 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,
* 当个数到5个时,线程2给出提示并结束线程2

思路:t1线程进行元素添加到容器里,t2线程满足条件后结束。

方式1:

  • 思路:t2在while(true)里轮询容器大小
  • 注意点:
  1. List对于2个线程存在不可见性,所以会导致线程2不会结束。这种不可见和while(true)那里一样,是指令重排导致的。
  2. volatile修饰List后,线程2才可以正确获取容器大小,才在满足条件时会结束
@Slf4j(topic = "console")
public class Container {
    // private List lists = new ArrayList();
    private volatile List lists = new ArrayList();
    public void add(Object o){
        lists.add(o);
    }
    public int size(){
        return lists.size();
    }
    public static void main(String[] args) {
        Container container = new Container();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                container.add(new Object());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("size = {}",container.size());
            }
            log.debug("end");
        },"t1").start();
        new Thread(() -> {
            while (true) {
                if (container.size() == 5) {
                    break;
                }
            }
            log.debug("end");
        },"t2").start();
    }
}

方式2:

  • 思路:
    对于方式1来说有一个很大的缺点,就是线程2在死循环,一直在running,浪费CPU性能。
    可以通过用锁,等待或唤醒来优化性能:
  • 一个wait,一个notify:
    t2启动拿到锁,不满足条件进行wait(睡眠状态),释放锁;
    t1拿到锁后,当满足条件的时候唤醒t2,但没有释放锁,所以t2处于阻塞状态,等到t1执行完毕结束后释放锁
    t2拿到锁后执行完毕。
  • 不断来回wait、notify:
    上面还是不满足需求呀,我们要container大小为5的时候t2执行完毕。
    所以需要在满足条件唤醒t2,t1把锁释放给t2。
    所以我们可以来回的睡眠唤醒来满足需求。
@Slf4j(topic = "console")
public class Container1 {

    private List lists = new ArrayList();
    public void add(Object o){
        lists.add(o);
    }
    public int size(){
        return lists.size();
    }

    public static void main(String[] args) throws InterruptedException {
        Container1 container = new Container1();
        //设一个锁
        Object lock = new Object();
        new Thread(() -> {
            log.debug("start");
            synchronized (lock) {
                if (container.size() != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("end");
                //唤醒t1
                lock.notify();
            }

            //释放锁
        },"t2").start();


        //为了让t2先调度,先拿到锁
        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            log.debug("start");
            synchronized (lock){
                for (int i = 0; i < 10; i++) {
                    container.add(new Object());
                    if (container.size() == 5) {
                        //唤醒t2
                        lock.notify();
                        try {
                            //t1睡眠,释放锁
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug("size = {}", container.size());
                }
            }
            log.debug("end");
        },"t1").start();
    }
}

方式3:

  • 思路:通过CountDownLatch来进行无锁化优化
  • 原因:当不涉及同步,只是涉及线程通信的时候,用synchronized加wait,notify就显得太重了

await和countDown来代替锁的wait和notify
CountDownLatch不涉及锁,当count的值为零时当前线程继续运行
相当于是发令枪,运动员线程调用await等待,计数到0开始运行

@Slf4j(topic = "console")
public class Container2 {

    private List lists = new ArrayList();

    public void add(Object o){
        lists.add(o);
    }

    public int size(){
        return lists.size();
    }

    public static void main(String[] args) throws InterruptedException {
        Container2 container = new Container2();
        //设一个门栓,只用倒计时1次
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(() -> {
            log.debug("start");
                if (container.size() != 5) {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("end");
        },"t2").start();


        //为了让t2先调度,先拿到锁
        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            log.debug("start");
                for (int i = 0; i < 10; i++) {
                    container.add(new Object());
                    if (container.size() == 5) {
                        latch.countDown();
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug("size = {}", container.size());
                }
            log.debug("end");
        },"t1").start();
    }
}