美文网首页java并发编程
你真的了解 synchronized 吗?

你真的了解 synchronized 吗?

作者: Loofer | 来源:发表于2017-09-24 13:17 被阅读35次

    线程安全概念

    当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
    synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"

    方法锁

    public class MyThread extends Thread{
        
        private int count = 5 ;
        
        //synchronized加锁
        public synchronized void run(){
            count--;
            System.out.println(this.currentThread().getName() + " count = "+ count);
        }
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            Thread t1 = new Thread(myThread,"t1");
            Thread t2 = new Thread(myThread,"t2");
            Thread t3 = new Thread(myThread,"t3");
            Thread t4 = new Thread(myThread,"t4");
            Thread t5 = new Thread(myThread,"t5");
            t1.start();
            t2.start();
            t3.start();
            t4.start();
            t5.start();
        }
    }
    
    
    

    当多个线程访问 myThreadrun 方法时,以排队的方式进行处理(这里排队是按照 CPU 分配的先后顺序而定的),一个线程想要执行synchronized 修饰的方法里的代码需要做两种操作:
    1、尝试获得锁
    2、如果拿到锁,执行synchronized代码体内容;拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题)

    对象锁

    public class MultiThread {
    
        private static int num = 0;
        
        /** static */
        public static 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();
            
        }
    }
    
    

    输出结果:

    方法printNum()未加static 后的输出结果:

    tag b, set num over!
    tag a, set num over!
    tag b, num = 200
    tag a, num = 100
    

    在方法printNum()加上static 后的输出结果:

    tag a, set num over!
    tag a, num = 100
    tag b, set num over!
    tag b, num = 200
    

    1、在本例中关键字 synchronized 取得的锁都是对象锁,而不是把一段代码(方法)当做锁,所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock
    2、在静态方法上加 synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)。

    对象锁异步同步的问题

    public class MyObject {
    
        public synchronized void method1(){
            try {
                System.out.println(Thread.currentThread().getName());
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        /** synchronized */
        public void method2(){
                System.out.println(Thread.currentThread().getName());
        }
        
        public static void main(String[] args) {
            
            final MyObject mo = new MyObject();
            
            /**
             * 分析:
             * t1线程先持有object对象的Lock锁,t2线程可以以异步的方式调用对象中的非synchronized修饰的方法
             * t1线程先持有object对象的Lock锁,t2线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步
             */
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    mo.method1();
                }
            },"t1");
            
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    mo.method2();
                }
            },"t2");
            
            t1.start();
            t2.start();
            
        }
        
    }
    

    输出结果:

    method2()未加synchronized关键字:

    t2
    t1
    

    method2()加上synchronized关键字:

    t1
    t2
    

    在本例中
    1、method2()方法未加synchronized关键字, t1 线程先持有object对象的Lock锁,t2 线程可以以异步的方式调用对象中的非synchronized修饰的方法 2、如果在method2()方法上加上synchronized关键字此时,t1 线程先持有object对象的Lock`锁,t2 线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步

    脏读、原子性问题

    在某些情况下业务整体需要使用完整的synchronized,保持业务的原子性。

    public class DirtyRead {
    
        private String username = "loofer";
        private String password = "123";
        
        public synchronized void setValue(String username, String password){
            this.username = username;
            
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            this.password = password;
            
            System.out.println("setValue最终结果:username = " + username + " , password = " + password);
        }
        
        public void getValue(){
            System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
        }
        
        
        public static void main(String[] args) throws Exception{
            
            final DirtyRead dr = new DirtyRead();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    dr.setValue("z3", "456");       
                }
            });
            t1.start();
            Thread.sleep(1000);
            
            dr.getValue();
        }
        
    }
    
    

    输出结果:

    getValue方法得到:username = z3 , password = 123
    setValue最终结果:username = z3 , password = 456
    

    由于使用了sleep()导致getValue()方法先执行,原本我们期望的应该是setValue()方法先执行,并且 password 应该都为 456 才对

    synchronized的重入

    例子1

    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();
        }
    }
    

    例子2

    public class SyncDubbo2 {
    
        static class Main {
            public int i = 10;
            public synchronized void operationSup(){
                try {
                    i--;
                    System.out.println("Main print i = " + i);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
        static class Sub extends Main {
            public synchronized void operationSub(){
                try {
                    while(i > 0) {
                        i--;
                        System.out.println("Sub print i = " + i);
                        Thread.sleep(100);      
                        this.operationSup();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
        public static void main(String[] args) {
            
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Sub sub = new Sub();
                    sub.operationSub();
                }
            });
            
            t1.start();
        }
    }
    

    输出结果:

    例子 1:

    method1..
    method2..
    method3..
    

    例子2:

    Sub print i = 9
    Main print i = 8
    Sub print i = 7
    Main print i = 6
    Sub print i = 5
    Main print i = 4
    Sub print i = 3
    Main print i = 2
    Sub print i = 1
    Main print i = 0
    

    synchronized 异常

    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();
        }
    }
    

    输出结果

    t1 , i = 1
    t1 , i = 2
    t1 , i = 3
    t1 , i = 4
    t1 , i = 5
    t1 , i = 6
    t1 , i = 7
    t1 , i = 8
    t1 , i = 9
    Exception in thread "t1" java.lang.RuntimeException
        at com.loofer.base.sync005.SyncException.operation(SyncException.java:18)
        at com.loofer.base.sync005.SyncException$1.run(SyncException.java:32)
        at java.lang.Thread.run(Thread.java:745)
    t1 , i = 10
    

    这里我们除了 main 线程外还有一个 t1 线程,当 t1 中抛出一个异常时,由于我们在operation()方法上加了锁,所以我们整个应用程序最后退出了,对于这种问题一般情况下可以采取 continue 写入日志发布告警,让应用程序继续执行。

    线程细节问题

    使用 synchronized 声明的方法在某些情况下是有弊端的,比如 A 线程调用同步的方法执行一个很长时间的任务,那么 B 线程必须等待比较长的时间才能执行,这样的情况下可以使用 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();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    changeLock.method();
                }
            },"t1");
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    changeLock.method();
                }
            },"t2");
            t1.start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t2.start();
        }
    }
    

    输出结果

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

    在本例中由于 lock 成员变量在线程执行的过程中被改变了,导致锁失效了

    死锁问题

    在设计程序时就应该避免双方相互持有对方的锁的情况

    public class DeadLock implements Runnable{
    
        private String tag;
        private static Object lock1 = new Object();
        private static Object lock2 = new Object();
        
        public void setTag(String tag){
            this.tag = tag;
        }
        
        @Override
        public void run() {
            if(tag.equals("a")){
                synchronized (lock1) {
                    try {
                        System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock1执行");
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock2) {
                        System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock2执行");
                    }
                }
            }
            if(tag.equals("b")){
                synchronized (lock2) {
                    try {
                        System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock2执行");
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock1) {
                        System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 进入lock1执行");
                    }
                }
            }
        }
        
        public static void main(String[] args) {
            
            DeadLock d1 = new DeadLock();
            d1.setTag("a");
            DeadLock d2 = new DeadLock();
            d2.setTag("b");
             
            Thread t1 = new Thread(d1, "t1");
            Thread t2 = new Thread(d2, "t2");
             
            t1.start();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t2.start();
        }   
    }
    

    输出结果

    当前线程 : t1 进入lock1执行
    当前线程 : t2 进入lock2执行
    

    在程序运行的运行时启动了两个线程,每个线程中都持有 lock1、lock2 两个锁,当 t1 运行时持有了 lock1 当 t2 运行的时候 lock2 被持有了,当 t1 运行到后面去尝试获得 lock2 就会一直等待,而 t2 继续运行尝试去获得 t1 发现被占用了也进入等待,这时就进入了互相等待的状态。也就是我们所说的死锁。

    同一对象属性的修改不会影响锁的情况

    public class ModifyLock {
        
        private String name ;
        private int age ;
        
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        
        public synchronized void changeAttributte(String name, int age) {
            try {
                System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 开始");
                this.setName(name);
                this.setAge(age);
                
                System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 修改对象内容为: " 
                        + this.getName() + ", " + this.getAge());
                
                Thread.sleep(2000);
                System.out.println("当前线程 : "  + Thread.currentThread().getName() + " 结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        public static void main(String[] args) {
            final ModifyLock modifyLock = new ModifyLock();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    modifyLock.changeAttributte("张三", 20);
                }
            },"t1");
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    modifyLock.changeAttributte("李四", 21);
                }
            },"t2");
            
            t1.start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t2.start();
        }
    }
    

    输出结果

    当前线程 : t1 开始
    当前线程 : t1 修改对象内容为: 张三, 20
    当前线程 : t1 结束
    当前线程 : t2 开始
    当前线程 : t2 修改对象内容为: 李四, 21
    当前线程 : t2 结束
    

    使用synchronized代码块减小锁的粒度,提高性能

    public class Optimize {
    
        public void doLongTimeTask(){
            try {
                
                System.out.println("当前线程开始:" + Thread.currentThread().getName() + 
                        ", 正在执行一个较长时间的业务操作,其内容不需要同步");
                Thread.sleep(2000);
                
                synchronized(this){
                    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) {
            final Optimize otz = new Optimize();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    otz.doLongTimeTask();
                }
            },"t1");
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    otz.doLongTimeTask();
                }
            },"t2");
            t1.start();
            t2.start();
            
        }
    }
    

    输出结果

    当前线程开始:t1, 正在执行一个较长时间的业务操作,其内容不需要同步
    当前线程开始:t2, 正在执行一个较长时间的业务操作,其内容不需要同步
    当前线程:t1, 执行同步代码块,对其同步变量进行操作
    当前线程:t2, 执行同步代码块,对其同步变量进行操作
    当前线程结束:t1, 执行完毕
    当前线程结束:t2, 执行完毕
    

    字符串加锁

    public class StringLock {
    
        public void method() {
            //new String("字符串常量")
            synchronized (new String("字符串常量")) {
                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) {
            final StringLock stringLock = new StringLock();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    stringLock.method();
                }
            },"t1");
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    stringLock.method();
                }
            },"t2");
            
            t1.start();
            t2.start();
        }
    }
    

    输出结果

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

    t1 永远持有锁,t2 永远进不去
    synchronized 代码块对字符串的锁,注意String常量池的缓存功能

    相关文章

      网友评论

        本文标题:你真的了解 synchronized 吗?

        本文链接:https://www.haomeiwen.com/subject/jirkextx.html