Java多线程——synchronized同步与volatile变量

本文详细解析了Java中synchronized关键字的使用方法及其特点,包括同步方法、同步块的概念和应用场景,同时还介绍了volatile关键字的作用及与synchronized的区别。

一、synchronized同步方法

1. “非线程安全”其实是在多个线程对同一对象中的实例变量进行并发访问时发生的。

2. “非线程安全”问题存在于“实例变量”中,如果是方法内部的局部变量,则不存在“非线程安全”问题,永远都是线程安全的,因为方法内部的局部变量是私有的,所以不存在数据共享。

为什么方法内部的局部变量是私有的,不存在数据共享?

        因为每当启用一个线程时,JVM就为他分配一个Java栈,栈是以帧为单位保存当前线程的运行状态。每当线程调用一个Java方法时,JVM就会在该线程对应的栈中压入一个帧(栈帧)。当执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等。不同的线程有不同地址的Java栈,那不同的线程为同一方法分配的栈帧也是不一样的,方法内部的局部变量是私有的,不存在数据共享

3. 同时满足以下要求才会出现“非线程安全问题”,才需要同步机制:

(1)多线程

(2)存在共享数据竞争

4. 以下情况都是线程安全的:

(1)单线程,即使存在共享数据

(2)不存在共享数据,即使多线程

5.  在两个线程访问同一个对象中的同步方法时一定是线程安全的。

6.  关键字synchronized获得的是对象锁,而不是把一段代码或方法(函数)当作锁。如果多个线程访问多个对象,则JVM会创建多个锁。

(1)A线程先持有Object对象的Lock锁,B线程可以以异步的方式调用Object对象中的非synchronized类型的方法,无需等待A线程释放Object对象的Lock锁;但如果方法之间存在共享数据,很可能会出现脏读。脏读就是在读取实例变量时,此值已经被其他线程更改过了。

(2)A线程先持有Object对象的Lock锁,这时如果B线程想要调用Object对象中的synchronized类型的方法,则需要等待A线程释放Object对象的Lock锁,B线程才能获取Object对象的Lock锁,这也就是同步。

(3)如果多个线程访问多个对象,可以以异步的方式运行。也就是A线程持有ObjectA对象的锁,同时B线程持有ObjectB对象的锁。

7.  synchronized锁重入

        关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁是是可以再次得到该对象锁的。这也证明了在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以成功获得此对象锁的。

“可重入锁”:就是自己可以再次获取自己的内部锁。

package synchronize;

/**
 * 关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁是是可以再次得到该对象锁的。
 * 这也证明了在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以成功获得此对象锁的。
 */
public class GetLockAgain extends Thread {

    @Override
    public void run() {
        Service ser = new Service();
        ser.service();
    }

    public static void main(String[] args) {
        GetLockAgain thread = new GetLockAgain();
        thread.start();
    }

}

//  Service类
class Service {

    public synchronized void service() {
        System.out.println("service()");
        service2();
    }

    public synchronized void service2() {
        System.out.println("service2()");
        service3();
    }

    public synchronized void service3() {
        System.out.println("service3()");
    }
}

当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。


8.   当一个线程执行的代码出现异常时,其所持有的锁会自动释放。然后其他线程可以获得此锁。

9.  同步不具有继承性。所以子类override父类的synchronized同步方法时,应该也要在子类的该方法加上synchronized修饰符,才能确保子类的该方法具有同步。


二、synchronized同步块

10. synchronized方法是对当前对象进行加锁,而synchronized{}同步代码块是对某一对象进行加锁。“某一对象”大多数情况下是实例变量及方法的参数。

        用关键字synchronized声明方法在某些情况下是有弊端的,会导致线程等待比较长时间。

11.  synchronized代码块间的同步块性:

        在使用同步synchronized(this)代码块时需要注意的是:当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是同一个对象。


12.   synchronized(非this对象  x),锁非this对象具有一定的优点:

        如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会收到阻塞,所以会影响效率;但如果使用同步代码块锁非this对象,则锁非this对象的同步块和锁this对象的方法是异步执行的,锁非this对象的同步块不与其他锁this方法争夺this锁,则可大大提高运行效率。

        由于对象监视器不同,所以就是异步运行的。

        同步代码块放在非同步synchronized方法中声明,并不能保证调用方法的线程的执行同步/顺序性,也就是线程调用方法的顺序是无序的,虽然在同步块中执行的顺序是同步的,这样极易出现“脏读”问题;使用“synchronized(非this对象  x)同步代码块”格式也可以解决“脏读”问题。


13. JVM执行遇到synchronized方法/块时,当前线程A会尝试获取对象锁L,如果此对象锁L已经被线程B占有,那线程A只能进入自旋等待或者阻塞状态;直至线程B释放对象锁L,线程A才有机会获取对象锁L。


14.  “synchronized(非this对象 x)”格式的写法是将x对象本身作为“对象监视器”,这样就可以得出以下3个结论:

(1)当多个线程对象同时执行synchronized(x){}同步代码块时呈同步效果;

(2)当其他线程执行x对象中的synchronized同步方法(即锁对象x)时呈同步效果;

(3)当其他线程执行x对象中的synchronized(this)同步代码块(即锁对象x)时呈同步效果;

但是,如果其他线程调用的是不加synchronized关键字的方法时,还是异步调用。


15.  synchronized关键字加到static静态方法上是给当前Class类进行加锁,

        synchronized关键字加到非static静态方法上是给当前对象进行加锁。


16.  如果线程A持有Class锁,线程B持有对象锁,那线程A和B是异步执行的。

        Class锁对类的所有对象实例都起作用。


17.  同步synchronized(类名.class)代码块的作用其实和synchronized static 方法的作用是一样的,都是对Class类加锁。


18. 由于String常量池的缘故,在大多数情况下,同步synchronized代码块都不使用String作为锁对象;而改用其他,比如说new Object()实例化一个Object对象,但它并不放入缓存中。


19.  同步synchronized方法容易造成死循环,可以用同步块解决:同步块持不同的锁对象。


20.  Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为这会造成线程假死。

        在设计程序时就要避免双方互相持有对方的锁的情况,因为此情况很有可能会导致死锁。

        只要互相等待对方释放锁就有可能出现死锁。


21.  内部类的实例化:

(1)非静态内部类:

OuterClass out=new OoterClass();  
InnerClass in=out.new InnerClass();// 通过外部类的实例化对象来实例化内部类 

(2)静态内部类

InnerClass in=new InnerClass();// 直接实例化


22. 内部类与同步

        内部类中的同步与普通类(外部类)的同步是一样的。


23.  锁对象的改变

        在将任何数据类型作为同步锁时,需要注意的是:是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的;如果分别获得不同的锁对象,那这些线程之间就是异步的。

        锁对象的属性被改变,并不会导致锁对象改变。


三、 volatile关键字

        关键字volatile的主要作用是使变量在多个线程间立即可见,但volatile关键字最致命的缺点是不支持原子性。多个线程之间的可见性实现的本质是:volatile关键字强制从公共堆栈(主内存)中取得变量的值,而不是从线程私有数据栈(工作内存)中取得变量的值。

24.  synchronized和volatile之间的区别:

(1)关键字volatile是同步线程的轻量级实现,volatile只能修饰变量;在某些情况下,volatile性能比synchronized要好。

(2)多线程访问volatile变量不会发生阻塞,而synchronized会出现阻塞。

(3)volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也能间接保证可见性,因为它将私有内存和共有内存中的数据做同步。

(4)关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。


25.  关键字volatile的使用场景:

   (1)

(2)


26.  线程安全包含原子性,可见性,有序性。

原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。

可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:程序执行的顺序按照代码的先后顺序执行。

PS:表达式i++不是原子性的,可分解为以下3个步骤:

        (1)从内存读取i的值;

        (2)计算i的值;

        (3)将i的值写入内存。


27.  使用原子类atomic进行i++操作

        原子操作是不能分割的整体,没有其他线程能够中断或者检t查正在原子操作的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全(thread-safe)。


28.  volatile变量和atomic变量的区别

(1)volatile变量,保证了数据在多个线程的可见性,但是并没有保证原子性,非线程安全。所以输出的结果比预期的200000小。

package compilerAndConcurrent;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * volatile或Atomic变量自增测试
 * volatile是变量修饰符,其修饰的变量具有可见性。
 */
public class VolatileTest implements Runnable {

     public static volatile int race = 0;

    public static void increase() {
       race++;
    }

    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            increase();
        }
    }

    private static final int THREADS_COUNTS = 20;

    // volatile 保证变量对所有线程的可见性
    public static void main(String[] args) throws InterruptedException {
        //线程数组
        VolatileTest vol = new VolatileTest();
        Thread[] threads = new Thread[THREADS_COUNTS];
        for (int i = 0; i < THREADS_COUNTS; i++) {
            // 匿名内部类
            threads[i] = new Thread(vol);
            threads[i].start();//启动线程
        }
        Thread.sleep(2000);
        System.out.println(race);
        Thread.sleep(10);
        System.out.println(race);
    }
}

输出:

165115
165115

(2)atomic类型变量就是一个原子操作,保证了原子性,线程安全。

package compilerAndConcurrent;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * volatile或Atomic变量自增测试
 * volatile是变量修饰符,其修饰的变量具有可见性。
 */
public class VolatileTest implements Runnable {

//        public static volatile int race = 0;
    public static AtomicInteger race = new AtomicInteger(0);//原子变量race

    public static void increase() {
//        race++;
        race.incrementAndGet();//自增1
    }

    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            increase();
        }
    }

    private static final int THREADS_COUNTS = 20;

    // volatile 保证变量对所有线程的可见性
    public static void main(String[] args) throws InterruptedException {
        //线程数组
        VolatileTest vol = new VolatileTest();
        Thread[] threads = new Thread[THREADS_COUNTS];
        for (int i = 0; i < THREADS_COUNTS; i++) {
            // 匿名内部类
            threads[i] = new Thread(vol);
            threads[i].start();//启动线程
        }
        Thread.sleep(2000);
        System.out.println(race);
        Thread.sleep(10);
        System.out.println(race);
/*        System.out.println(race.compareAndSet(200000,11));
        System.out.println(race);*/
    }
}

输出:

200000
200000
        atomic原子类型变量的方法(比如说addAndGet)是原子的,但方法和方法之间的调用却不是原子的,所以需要进行同步处理,不然会导致非线程安全。这种类型称为相对线程安全,AtomicInteger,AtomicLong,Vector等都是相对线程安全。


29.   关键字synchronized可以保证同一时刻,只有一个线程可以执行某一方法或某一个代码块。它包含两个特征:互斥性和可见性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值