底层实现

2022-08-06 15:55:49 浏览数 (1)

底层实现

volatile 关键字

修饰成员变量,每次被线程访问时,强迫从主存中读写该成员变量的值。

volatile 关键字只能保证可见性,不能保证原子性。多个线程同时操作主内存里的同一个变量时,变量数据仍有可能会遭到破坏。

  • 线程执行过程中如果 CPU 一直满载运转,就会默认使用本地内存中的值,而没有空闲读取主存同步数据。
  • 线程执行过程中一旦 CPU 获得空闲,JVM 也会自动同步主存数据,尽可能保证可见性。
代码语言:javascript复制
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        try { 
            Thread.sleep(1000); 
        } catch(InterruptedException e) {}
        t.setRun(false);      
    }
}

class MyThread extends Thread {
    // 添加 volatile 关键字,强制同步主存数据。
    // 删除 volatile 关键字,子线程将始终读取本地内存中 true 副本:陷入死循环。
    private volatile boolean run = true;             
    public void setRun(boolean run) { this.run = run; }
    @Override
    public void run() {
        while (this.run == true) {
            int a = 2;
            int b = 3;
            int c = a   b;
            // System.out.print("CPU rest");      打印输出时 CPU 获得空闲,自动同步主存数据。
        }
        System.out.print("end");   
        return;
    }
}Copy to clipboardErrorCopied

synchronized 关键字

修饰方法或代码块。被线程访问时由线程抢占锁,直到执行完毕后自动释放锁。其他线程没有获得锁将无法访问上锁内容。保证了指定内容在同一时刻只有一个线程能访问。

  1. 修饰 static 方法实质是给当前类上锁:这个类的所有 synchronized static 方法共享一个锁。
  2. 修饰实例方法实质是给对象上锁:这个对象内所有的 synchronized 实例方法共享一个锁。

每一个对象都有且仅有一个与之对应的 monitor 对象。synchronized 关键字修饰方法时会对方法添加标志位,当线程执行到某个方法时,JVM会去检查该方法的访问标志是否被设置,如果设置了线程会先获取这个对象所对应的 monitor 对象,再执行方法体,方法执行完后释放 monitor 。

同步代码块则是在同步代码块前插入 monitorenter ,在同步代码块结束后插入 monitorexit 。

代码语言:javascript复制
public class ThreadDemo {
    public static void main(String[] args) {
        ThreadDemo test = new ThreadDemo();
        new Thread(test::m1).start();
        new Thread(test::m2).start();   
    }

    public synchronized void m1() {
        System.out.println("1");
        try { 
            Thread.sleep(1000); 
        } catch(InterruptedException e) {}
        System.out.println("2");
    }

    public synchronized void m2() {
        System.out.println("3");
        try { 
            Thread.sleep(500); 
        } catch(InterruptedException e) {}
        System.out.println("4");
    }
}Copy to clipboardErrorCopied
同步对象

创建两个不同的对象就拥有两把不同的锁,不同对象的 synchronized 实例方法互不影响。

代码语言:javascript复制
public class ThreadDemo {
    public static void main(String[] args) {
        ThreadDemo test1 = new ThreadDemo();
        ThreadDemo test2 = new ThreadDemo();
        new Thread(test1::m1).start();
        new Thread(test2::m2).start(); 
    }

    public synchronized void m1() {
        System.out.println("1");
        try { Thread.sleep(1500); } catch(InterruptedException e) {}
        System.out.println("4");
    }

    public synchronized void m2() {
        try { Thread.sleep(500); } catch(InterruptedException e) {}
        System.out.println("2");
        try { Thread.sleep(500); } catch(InterruptedException e) {}
        System.out.println("3");
    }
}Copy to clipboardErrorCopied
同步方法

其他线程无法获取该对象锁,就不能访问该对象的所有 synchronized 实例方法,但仍可以访问其他方法。 synchronized 实例方法中调取的数据仍可能被其他方法修改。

在实际开发过程中,我们常常对写操作加锁,但对读操作不加锁,提升系统的并发性能。但可能会导致脏读问题。

代码语言:javascript复制
public class ThreadDemo {
    public static void main(String[] args) {
        ThreadDemo test = new ThreadDemo();
        new Thread(test::m1).start();
        new Thread(test::m2).start(); 
    }

    boolean data = false;

    public synchronized void m1() {
        System.out.println(data);            // false
        try { 
            Thread.sleep(1000); 
        } catch(InterruptedException e) {}
        System.out.println(data);             // true
    }

    public void m2() throws {
        try { 
            Thread.sleep(500); 
        } catch(InterruptedException e) {}
        this.data = true;
    }
}Copy to clipboardErrorCopied
同步代码块

如果我们需要同步的代码只有一小部分,就没有必要对整个方法进行同步操作,我们只需要同步的代码块进行包裹。

修饰代码块,需要指定被上锁的对象或者类。每次线程进入 synchronized 代码块时就会要求当前线程持有该对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行操作。我们通常使用 this 对象或者当前类的 class 对象作为锁。

不要以字符串对象作为锁的对象。字符串常量在常量池里被锁定,可能会导致意想不到的阻塞。

代码语言:javascript复制
public class ThreadDemo {
    public static void main(String[] args) {
        ThreadDemo test = new ThreadDemo();
        new Thread(test::m1).start();
        new Thread(test::m2).start();   
    }

    public void m1() {
        synchronized(this) {
            System.out.println("1");
            try { 
                Thread.sleep(1000); 
            } catch(InterruptedException e) {}
            System.out.println("2");
        }  
    }

    public synchronized void m2() {
        synchronized(this) {
            System.out.println("3");
            try { 
                Thread.sleep(500); 
            } catch(InterruptedException e) {}
            System.out.println("4");
        }
    }
}Copy to clipboardErrorCopied

线程执行代码出现异常时也会自动释放所有锁,因此在 synchronized 内部处理异常一定要非常小心。如果不想释放锁,使用 try-catch 语句捕获异常。

两者的区别

  1. volatile 关键字用于修饰变量,synchronized 关键字用于修饰方法以及代码块。
  2. volatile 关键字是数据同步的轻量级实现,性能比 synchronized 关键字更好。
  3. volatile 关键字被多线程访问不会发生阻塞,synchronized 关键字可能发生阻塞。
  4. volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
代码语言:javascript复制
// 双重锁结构实现单例模式

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {    
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}

0 人点赞