使用synchronized解决多线程安全问题

目录

多线程安全问题:

解决多线程下安全问题的方法:

      1.使用synchronized关键字:

       2.对共享区代码使用显式Lock对象加锁:

什么是对象锁

使用浅显易懂的例子说明synchronized对象锁:

对象锁实现的方式:

什么是类锁:

使用synchronized关键字实现类锁:

 总结:


多线程安全问题:

首先,在操作系统中线程是不拥有资源的,系统按进程分配所有除CPU以外的系统资源(如主存,外设,文件等等),而程序是依赖于线程运行,系统按照线程分配CPU资源。进程只是作为除CPU以外的系统资源的分配单位。在一个进程中至少有一个线程,这些线程共享程序区,数据区,但是它们有各自的运行栈区(用于保存其运行现场,在线程调度时使用),可以被独立的调度占用CPU并行或者并发运行。

并发:分时占用处理机(在一个CPU中);

并行:同时占用不同处理机(至少发生在两个CPU中);

同步:计算机中"同步"二字里面的"同"并不是字面上的动作一起,而是指协同,协助,相互配合。例如一个进程(或线程)需要向另一个进程(或线程)传递数据,也就是说后面的进程(或线程)必须等待前面的进程(或线程)的数据到达之后才能继续运行,这种在进程(或线程)间存在的关系就叫做同步

CPU以线程为单位进行调度。在某一个点系统只会有一个线程去执行任务,下一个时间点有可能又会切换为其他线程去执行任务,(只是因为CPU利用时间轮转片这种方式作业的时候它在多个线程或者多个进程之间切换的很快,给人的感觉是像多个进程或线程在同时工作罢了)。我们没有办法预测某一个时刻究竟是哪个线程在执行,这是由CPU统一调度的。有时候我们的程序中的部分代码是多个线程共享使用的,多个线程都可以对部分数据进行读写,我们把这一类的代码叫做临界区。这样子就会出现安全性的问题:如果多条语句要共享给多个线程使用,而如果在一个线程只执行了这多条语句中的一部分,还没有把它们都执行完的时候,另外一个线程获取了CPU资源并参与进来对共享代码进行执行。这就可能会导致共享数据差距产生错误。

例:以下代码被线程A,B共享:

s[0] = ...;
s.length++;

若当线程A执行完第一行开始执行但还没有执行第二行的时候,CPU调度线程A挂起,线程B获得了CPU的资源,于是线程B开始执行共享区的代码,线程A再次获取CPU资源的时候线程B已经将这两行代码全部执行完毕,由于线程A已经执行了第一行代码,所以当它再次获取CPU资源的时候开始执行第二行代码,也就是长度加一。那么问题来了:线程A在数组下标为0的地方存储的数据被线程B掩盖了,所以此时的s[0]存储的线程B赋予它的值,但是长度却被线程A,B分别加一,即为二。这就是多线程下由于多个线程同时操作共享区而导致的数据发生错误。

【注意】:这里的“操作”指的是写,如果多个线程同时去“读”,就不会出现线程不安全的问题。

解决多线程下安全问题的方法:

      1.使用synchronized关键字:

     synchronized是java中的关键字,是内置语言的实现。

       2.对共享区代码使用显式Lock对象加锁:

     java.util.concurrent类库包含定义在java.util.concurrent.locks中的显式的互斥机制。

这篇博客只介绍第一种方法,即使用synchronized关键字解决多线程并发时的安全性问题。

什么是对象锁

对象锁又称为方法锁。对于对象锁来说它是针对于对象的。它在该对象的某个内存位置声明一个标志位来标识该对象是否具有对象锁。所以它只会锁住特定的对象。

一般来讲:对象锁是对一个非静态成员变量进行syncronized修饰,或者对一个非静态方法进行syncronized修饰。它"锁"住的是某个类的类对象,多个线程来同时执行这个类中的synchronized 修饰的方法(仅限被synchronized修饰的方法,如果多个线程同时执行类中不被synchronized修饰的方法不会产生互斥的现象)的时候,就会出现互斥,只有一个获得了对象锁的线程可以进入此方法执行,其它的线程必须等待此线程执行完毕并且释放对象锁之后才可以获取对象锁并且执行被synchronized修饰的方法,如下:

/**
 * @author - ZwZ
 * @date - 2018/12/6 - 18:39
 * @Description:实现了Runnable接口的类
 */
public class Synchronized3 implements Runnable {
    public synchronized void test() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 到此一游");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程 " + Thread.currentThread().getName() + " 滚蛋了");
    }
    @Override
    public void run() {
        test();
    }
}
/**
 * @author - ZwZ
 * @date - 2018/12/6 - 18:42
 * @Description:5通过输出结果对synchronized修饰的方法进行测试
 */
public class Test {
    public static void main(String[] args) {
        //线程池
        ExecutorService executorPool = Executors.newFixedThreadPool(5);
        //开启线程池中的5个线程
        for (int i = 0; i <5 ; i++) {
            executorPool.submit( new Synchronized3());
        }
    }
}

其输出结果如下:

线程 pool-1-thread-1 到此一游
线程 pool-1-thread-2 到此一游
线程 pool-1-thread-3 到此一游
线程 pool-1-thread-4 到此一游
线程 pool-1-thread-5 到此一游
(以上五行代码同时输出)
(这中间有1秒的等待时间 因为在类Synchronized3中的两条输出语句之间线程沉睡了1000ms)
(以下五行代码同时输出)
线程 pool-1-thread-1 滚蛋了
线程 pool-1-thread-2 滚蛋了
线程 pool-1-thread-3 滚蛋了
线程 pool-1-thread-5 滚蛋了
线程 pool-1-thread-4 滚蛋了

可以看出,线程池中的五个线程是同时到达,synchronized貌似并没有起到该有的同步的作用。我们把test中的代码换掉:

/**
 * @author - ZwZ
 * @date - 2018/12/6 - 18:42
 * @Description:第二次测试,使用同一个Synchronized3对象
 */
public class Test {
    public static void main(String[] args) {
        //线程池
        ExecutorService executorPool = Executors.newFixedThreadPool(5);
        Synchronized3 synchronized3 = new Synchronized3();
        for (int i = 0; i <5 ; i++) {
            executorPool.submit(synchronized3);
        }
    }
}

 把Synchronized3类中增加一个test2( ) (目的是用来证明对象锁只对这个对象中使用synchronized关键字修饰的方法来说的,对于没有被synchronized修饰的关键字,允许多个线程同时执行) :

/**
 * @author - ZwZ
 * @date - 2018/12/6 - 18:39
 * @Description:实现了Runnable接口的类
 */
public class Synchronized3 implements Runnable {
    public synchronized void test() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 到此一游");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程 " + Thread.currentThread().getName() + " 滚蛋了");
    }

    public void test2() {
        System.out.println("这个方法没有被synchronized修饰,所以线程:" + Thread.currentThread().getName() + " 可以在没有获得对象锁的时候执行它");
    }

    @Override
    public void run() {
        test();
        test2();
    }
}

 其运行结果如下:

线程 pool-1-thread-1 到此一游 (进程1得到了对象锁)
线程 pool-1-thread-1 滚蛋了 (此时进程1把对象锁释放了)
线程 pool-1-thread-5 到此一游 (这个说明进程5获得了刚刚进程1释放的对象锁)
这个方法没有被synchronized修饰,所以线程:pool-1-thread-1 可以在没有获得对象锁的时候执行它 (由这行代码可知进程5得到对象锁的时候进程1仍然在执行test2()的代码,这说明进程1是在执行完synchronized修饰的test()之后,还没有执行test2()的时候将对象锁释放的)
线程 pool-1-thread-5 滚蛋了
这个方法没有被synchronized修饰,所以线程:pool-1-thread-5 可以在没有获得对象锁的时候执行它
线程 pool-1-thread-4 到此一游
线程 pool-1-thread-4 滚蛋了
线程 pool-1-thread-3 到此一游
这个方法没有被synchronized修饰,所以线程:pool-1-thread-4 可以在没有获得对象锁的时候执行它
线程 pool-1-thread-3 滚蛋了
这个方法没有被synchronized修饰,所以线程:pool-1-thread-3 可以在没有获得对象锁的时候执行它
线程 pool-1-thread-2 到此一游
线程 pool-1-thread-2 滚蛋了
这个方法没有被synchronized修饰,所以线程:pool-1-thread-2 可以在没有获得对象锁的时候执行它

 根据控制台打印输出的结果看出,这次synchronized关键字发挥了作用,Synchronized3类中的test( )方法在同一时刻只有一个线程进入并执行其代码。

那么原因是什么?为什么把Test类中的代码更换掉之后synchronized关键字就发挥了作用?接下来我们分析以下刚刚在Test类中更换了哪些代码:

第一次:

//线程池
ExecutorService executorPool = Executors.newFixedThreadPool(5);
//开启线程池中的5个线程
for (int i = 0; i <5 ; i++) {
   executorPool.submit( new Synchronized3());
}

 第二次:

//线程池
ExecutorService executorPool = Executors.newFixedThreadPool(5);
Synchronized3 synchronized3 = new Synchronized3();
for (int i = 0; i <5 ; i++) {
   executorPool.submit(synchronized3);
}

 这两次区别在于:第一次开启线程的时候每次都创建了一个Synchronized3类对象,但是第二次开启线程的时候使用的是同一个Synchronized3类对象。之所以两次结果截然不同就是因为:synchronized关键字是"锁"的Synchronized3类的对象,而第一次使用了五个Synchronized3类对象,同一个类的每个对象的同步锁之间都是没有联系的,所以五个线程之间不存在同步。所以可以证明这里面使用的synchronized属于对象锁,它把某一个对象锁住了,线程要想执行其中的被synchronized关键字修饰的方法,只能是在它获取对象锁的时候。

并且通过test2( )打印输出的结果,也说明了对象锁只是针对多个线程同时执行synchronized关键字修饰的方法的时候才会起作用,多个线程并发执行某一个对象中没有使用synchronized关键字修饰的方法的时候是可以同时进入这个方法体进行执行的,也就是没有互斥这一说。

使用浅显易懂的例子说明synchronized对象锁:

把一个类对象比作成一个大四合院,这个四合院的大门永远是开着的 (这里说个题外话:为什么java中的外部类只能被public和default修饰?因为java中的外部类只能被public和默认的default修饰,原因在于外部类的上一个单元是包,所以外部类只有两个作用域:同包和任意位置,因此只需要两种控制权限:包控制权限和公开访问控制权限,也就是对应的default和public,如果只有内部类才可以使用private修饰符,因为内部类的上一级是外部类,所以它对应有四种访问修饰符:本类即private,同包deault,父子类protectd,任何位置public) ,四合院里面有很多的小房间(也就是方法),有些小房间上了锁(有synchronized修饰),有些则没有。而这些上了锁的房间共用一把钥匙(key),并且它放在了四合院的门口的一个信箱里面。

那些进出四合院中的某一个房间的人就好比是执行了这个类对象中的某一个方法的线程。一个人想进入某间上了锁的房间,他来到四合院门口,看见钥匙在信箱中(说明暂时还没有其他人要进入上锁的房间)。于是他拿到钥匙,并且按照自己 的计划进入那些房间。注意一点,他每次使用完一次上锁的房间后会马上把钥匙还回去。即使他要连续使用两间上锁的房间,中间他也要把钥匙还回去。

在刚刚这个人使用上锁的房间的同时,其他人可以不受限制的使用那些不上锁的房间,一个人用一间可以,两个人用一间也可以,没有任何限制。但是如果当某个人想要进入上锁的房间,他就要跑到大门口去看看信箱中有没有钥匙,有钥匙拿了就走,没有的话,就只能等了。

至于有很多人在等这把钥匙,等钥匙还回来以后,谁会优先得到钥匙。这个是不确定的,可以理解为随机的(其实计算机中的随机并不是真正意义上的随机,是人运用一定的方法写出来的,看上去随机罢了)

对象锁实现的方式:

1.synchronized修饰方法(上边的演示代码就属于这种方式):

这种实现synchronized同步锁的方式是有缺点的,它效率低下,因为如果一个类中有很多个synchronized修饰的方法,线程A调用这个类中使用syncronized修饰的testA( ),线程B调用这个类中使用syncronized修饰的testB( ),那么当线程A拿到对象锁去执行testA( )的时候,线程B不能去执行testB( ),因为这两个方法共用一个对象锁。举例说明:

/**
 * @author - ZwZ
 * @date - 2018/12/6 - 21:43
 * @Description:一个类中两个使用synchronized修饰的方法
 * 会使得效率变低
 */
public class SynchronizeTest {
    public synchronized void testA() {
        System.out.println("此时线程"+Thread.currentThread().getName()+"拿到对象锁");
        System.out.println("线程"+Thread.currentThread().getName()+"开始干活");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("此时线程"+Thread.currentThread().getName()+"释放了对象锁");
    }

    public synchronized void testB() {
        System.out.println("此时线程"+Thread.currentThread().getName()+"拿到对象锁");
        System.out.println("线程"+Thread.currentThread().getName()+"开始干活");
        System.out.println("此时线程"+Thread.currentThread().getName()+"释放了对象锁");
    }
}
/**
 * @author - ZwZ
 * @date - 2018/12/6 - 21:46
 * @Description:测试
 */
public class Test2 {
    public static void main(String[] args) {
        SynchronizeTest synchronizeTest = new SynchronizeTest();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizeTest.testA();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizeTest.testB();
            }
        });
        thread1.start();
        thread2.start();
    }
}

 输出结果如下:

此时线程Thread-0拿到对象锁
线程Thread-0开始干活
(在这里有3000ms的间隔,因为进程睡眠了3s)
此时线程Thread-0释放了对象锁
(上边这一行代码输出之后,立即有了下面这一行输出)
此时线程Thread-1拿到对象锁
线程Thread-1开始干活
此时线程Thread-1释放了对象锁

 由输出结果可以看出:进程Thread-1一直等到Thread-0释放对象锁之后才可以执行testB(),又因为执行testA()的进程Thread-0睡眠了3s,所以导致Thread-1等待了它好久。

最最关键的是:这两个线程分别执行的testA()和testB()两个方法之间没有任何的关系,它们没有必要同步执行,就算是异步执行也不会有什么数据安全上的影响(前提是,在这两个方法的方法体中没有对一些共享数据进行操作)。所以,Thread-1白等了Thread-0。

用在刚刚的例子上就是:一开始我们给房间上锁的目的是为了避免多个人进入同一个房间共用一些不可以共用的东西而造成不安全的事情发生,但是如果有些人压根就不是想进入同一个房间,而是想进入的四合院中的不同的房间,来使用属于每个房间的,而不是共享性的东西。那么如果还是在每一个房间上加个锁的话,未免会导致效率过于低下。

更过分点儿,一个人拿着要是进入了一个上锁的房间里面,结果在里面死掉了(某个方法里面含有死循环),而在外边等着他还钥匙的人并不知道他在里面死掉了,只知道在这里等,一直等到他还钥匙过来。那这样子的话,岂不是要等到天荒地老?

2.synchronized修饰代码块:

synchronized除了上面那样修饰方法外,还可以修饰代码块,也就是修饰方法中的一块代码,这就是通常所说的减小锁的粒度,被修饰的代码块叫做同步代码块。它在一定程度上弥补了synchronized修饰方法时候产生的效率低下等问题。

synchronized修饰代码块的格式:

//表示对括号中的任意对象加锁
//这里的任意对象指的是共享变量
synchronized(任意对象){
   ......
}

我们经常看到如下这种形式的代码:

synchronized(this){
    ......
}

 这个的意思就是对this即当前对象加锁,又因为加在方法上的synchronized关键字,即”锁住这个方法“其实就是在锁住这个方法所在的类的这个对象。所以在同一个类中synchronized(this){ }和synchronized修饰方法共用一把锁:这个方法和synchronized(this){ }这个代码块所在的类的对象的对象锁。结合下面的例子说明:

/**
 * @author - ZwZ
 * @date - 2018/12/6 - 21:43
 * @Description:
 */
public class SynchronizeTest {
    private String string="shabi";
    public synchronized void testA() {
        System.out.println("此时线程"+Thread.currentThread().getName()+"拿到对象锁");
        System.out.println("线程"+Thread.currentThread().getName()+"开始干活");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("此时线程"+Thread.currentThread().getName()+"释放了对象锁");
    }

    public void testB() {
        System.out.println("hhh");
        synchronized (this) {
            System.out.println("此时线程" + Thread.currentThread().getName() + "拿到对象锁");
            System.out.println("线程" + Thread.currentThread().getName() + "开始干活");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("此时线程" + Thread.currentThread().getName() + "释放了对象锁");
        }
    }
}
/**
 * @author - ZwZ
 * @date - 2018/12/6 - 21:46
 * @Description:测试
 */
public class Test2 {
    public static void main(String[] args) {
        SynchronizeTest synchronizeTest = new SynchronizeTest();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizeTest.testA();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizeTest.testB();
            }
        });
        thread1.start();
        thread2.start();
    }
}

 其运行结果如下:

此时线程Thread-0拿到对象锁
线程Thread-0开始干活
hhh
此时线程Thread-0释放了对象锁
此时线程Thread-1拿到对象锁
线程Thread-1开始干活
此时线程Thread-1释放了对象锁
(1,2,3行同时输出,五秒后;4,5,6行同时输出;一秒后7行输出)

 根据上面的运行结果可以看出:不使用synchronized(this){...}和synchronized修饰方法它们共用同一个锁,那就是这个方法所在的类的对象锁。我个人认为它俩最大的区别就在于:synchronized(this){...}用在方法内,对于方法中没有必要加锁的代码可以写在synchronized(this){...}外面,减小了锁粒度。但是synchronized修饰方法就要把方法中的所有代码都包含。

我们还经常看到一种形式:

synchronized(String等类型的变量){
    ......
}

这个的好处就在于:它是对于指定的变量进行加锁,而不是对这个代码块所在的方法所在的类的对象进行加锁。如果这个代码块所在的方法所在的类中存在synchronized修饰的方法,那么这个加了锁的代码块不会影响这些synchronized修饰的方法的执行,可以提高效率。

举例说明:

/**
 * @author - ZwZ
 * @date - 2018/12/7 - 1:09
 * @Description:
 */
public class SynchronizedTest4 {
    public Person person = new Person();
    public synchronized void test1(){
        System.out.println("我"+Thread.currentThread().getName()+" 用的是对象锁");
        try {
            Thread.sleep(9000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName()+":睡醒了");
    }
    public void test2(int age,String name){
        /*对person对象加锁
        进了线程执行test2()时,谁拿到person对象的锁,就会执行代码块内的代码
        否则等待*/
        synchronized (person){
            person.setAge(age);
            person.setName(name);
            System.out.println("当前线程是:"+Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("personAge"+person.getAge());
            System.out.println("personName"+ person.getName());

        }
    }
}
/**
 * @author - ZwZ
 * @date - 2018/12/7 - 1:12
 * @Description:
 */
public class Test4 {
    public static void main(String[] args) {
        SynchronizedTest4 synchronizedTest4 = new SynchronizedTest4();
        //线程1
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest4.test1();
            }
        });
        //线程2
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest4.test2();
            }
        });
        //线程3
        Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest4.test2();
            }
        });
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

输出结果如下:

 上边的代码完全可以把synchronized(person){...}代码块去掉,然后在test2()方法上加上关键字synchronized。但是这样子test1()和test2()就会共享一个对象锁。在多线程高并发的环境下效率低并且在特殊环境下可能会出问题。

【注意】:使用synchronized(String等类型的变量){...}的时候必须把加锁的变量使用private修饰。否则的话别的类可以通过对象直接调用这个变量的话,也就失去了加锁的意义。

什么是类锁:

与对象锁的原理大致相同,但是它锁的是某一个类的Class对象,可以认为是锁住了整个类,对于这个类所有的实例化对象都会起作用,所以叫做类锁。

使用synchronized关键字实现类锁:

1.synchronized static修饰方法:

/**
 * @author - ZwZ
 * @date - 2018/12/7 - 1:59
 * @Description:类锁
 */
public class SynchronizedTest5 implements Runnable {
    public synchronized static void test1() {
        System.out.println("当前线程为:" + Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        test1();
    }
}
/**
 * @author - ZwZ
 * @date - 2018/12/7 - 2:02
 * @Description:测试
 */
public class Test5 {
    public static void main(String[] args) {
        //线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new SynchronizedTest5());
        threadPool.submit(new SynchronizedTest5());
        threadPool.submit(new SynchronizedTest5());
    }
}

 输出结果:

当前线程为:pool-1-thread-1
(间隔2000ms)
当前线程为:pool-1-thread-3
(间隔2000ms)
当前线程为:pool-1-thread-2

 每一个线程都是使用的一个SynchronizedTest5的类对象,但是输出的时候依旧有间隔时间,就是因为:类SynchronizedTest5()中的方法test1()被static和synchronized同时修饰。被static修饰说明test1()这个方法已经不属于类的某一个对象了,而是属于整个类,而synchronize修饰被static修饰的方法也就意味着它锁的是SynchronizedTest5类的Class对象(每一个类都有一个Class类的对象)。所以虽然每个线程都是使用的不同的SynchronizedTest5的对象,但是也都会受到类锁的影响。

 2.使用synchronized(类名.class){...}的形式 :

例如把上边的test1( )改成下面这种形式(会有与上边同样的输出效果):

public void test1() {
        synchronized (SynchronizedTest5.class) {
            System.out.println("当前线程为:" + Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

 因为“类名.class”就是得到指定类的Class对象,而将其写在代码块中就是对指定类的Class对象进行加锁。

 总结:

1.synchronized关键字是用来解决多线程并发时的安全问题,如果说是单线程环境下,它毫无意义,还会影响效率;

2.要使用关键字synchronized,线程之间必须存在同步写数据,如果只是同步读数据的话,没有必要使用锁,因为不会出现数据安全性问题。

//后期会在把如何使用显式Lock对象加锁专门分享一篇博客,可以加关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值