Java高级特性 - 多线程基础(3)线程同步

使用synchronized关键字同步线程

任务描述

本关任务:使右侧代码中的insert方法在同一时刻只有一个线程能访问。

相关知识

为了完成本关任务,你需要掌握:

1.并发编程什么时候会出现安全问题;

2.怎么解决线程安全问题;

3.synchronized关键字。

并发编程什么时候会出现安全问题

在单线程的时候是不会出现安全问题的,不过在多线程的情况下就很有可能出现,比如说:多个线程同时访问同一个共享资源,多个线程同时向数据库插入数据,这些时候如果我们不做任何处理,就很有可能出现数据实际结果与我们预期的结果不符合的情况。

现在有两个线程同时获取用户输入的数据,然后将数据插入到同一张表中,要求不能出现重复的数据。

我们必然要在插入数据的时候进行如下操作:

  • 检查数据库中是否存在该数据;
  • 如果存在则不插入,否则插入。

现在有两个线程ThreadA和ThreadB来对数据库进行操作,当某个时刻,线程A和B同时读取到了数据X,这个时候他们都去数据库验证X是否存在,得到的结果都是不存在,然后A、B线程都向数据库插入了X数据,这个时候数据库中出现了两条X数据,还是出现了数据重复。

这个就是线程安全问题,多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果

这里面,这个资源被称为:临界资源(也可以叫共享资源)。

当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等等)时,就有可能产生线程安全问题。

当多个线程执行一个方法时,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。

如何解决线程安全问题

怎么解决线程的安全问题呢?

基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称作同步互斥访问。

在Java中一般采用synchronized和Lock来实现同步互斥访问。

synchronized关键字

首先我们先来了解一下互斥锁,互斥锁:就是能达到互斥访问目的的锁。

如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。

在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。

在我们编写代码的时候,可以使用synchronized修饰对象的方法或者代码块,当某个线程访问这个对象synchronized方法或者代码块时,就获取到了这个对象的锁,这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行该对象的方法。

我们来看个示例进一步理解synchronized关键字:

  1. public class Example {
  2.  
  3. public static void main(String[] args)  {
  4. final InsertData insertData = new InsertData();
  5.  
  6. new Thread() {
  7. public void run() {
  8. insertData.insert(Thread.currentThread());
  9. };
  10. }.start();
  11.  
  12. new Thread() {
  13. public void run() {
  14. insertData.insert(Thread.currentThread());
  15. };
  16. }.start();
  17. }
  18. }
  19.  
  20. class InsertData {
  21. private ArrayList<Integer> arrayList = new ArrayList<Integer>();
  22.  
  23. public void insert(Thread thread){
  24. for(int i=0;i<5;i++){
  25. System.out.println(thread.getName()+"在插入数据"+i);
  26. arrayList.add(i);
  27. }
  28. }
  29. }

这段代码的执行是随机的(每次结果都不一样):

Thread-0在插入数据0 Thread-1在插入数据0 Thread-1在插入数据1 Thread-1在插入数据2 Thread-1在插入数据3 Thread-1在插入数据4 Thread-0在插入数据1 Thread-0在插入数据2 Thread-0在插入数据3 Thread-0在插入数据4

现在我们加上synchronized关键字来看看执行结果:

  1. public synchronized void insert(Thread thread){
  2.  for(int i=0;i<5;i++){
  3. System.out.println(thread.getName()+"在插入数据"+i);
  4. arrayList.add(i);
  5. }
  6. }

输出:

Thread-0在插入数据0 Thread-0在插入数据1 Thread-0在插入数据2 Thread-0在插入数据3 Thread-0在插入数据4 Thread-1在插入数据0 Thread-1在插入数据1 Thread-1在插入数据2 Thread-1在插入数据3 Thread-1在插入数据4

可以发现,线程1会等待线程0插入完数据之后再执行,说明线程0和线程1是顺序执行的。

从这两个示例中,我们可以知道synchronized关键字可以实现方法同步互斥访问。

在使用synchronized关键字的时候有几个问题需要我们注意:

  1. 在线程调用synchronized的方法时,其他synchronized的方法是不能被访问的,道理很简单,一个对象只有一把锁;
  2. 当一个线程在访问对象的synchronized方法时,其他线程可以访问该对象的非synchronized方法,因为访问非synchronized不需要获取锁,是可以随意访问的;
  3. 如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

synchronized代码块

synchronized代码块对于我们优化多线程的代码很有帮助,首先我们来看看它长啥样:

  1. synchronized(synObject) {
  2.  
  3. }

当在某个线程中执行该段代码时,该线程会获取到该对象的synObject锁,此时其他线程无法访问这段代码块,synchronized的值可以是this代表当前对象,也可以是对象的属性,用对象的属性时,表示的是对象属性的锁。

有了synchronized代码块,我们可以将上述添加数据的例子修改成如下两种形式:

  1. class InsertData {
  2. private ArrayList<Integer> arrayList = new ArrayList<Integer>();
  3.  
  4. public void insert(Thread thread){
  5. synchronized (this) {
  6. for(int i=0;i<100;i++){
  7. System.out.println(thread.getName()+"在插入数据"+i);
  8. arrayList.add(i);
  9.     }
  10. }
  11. }
  12. }
  13. class InsertData {
  14. private ArrayList<Integer> arrayList = new ArrayList<Integer>();
  15. private Object object = new Object();
  16.  
  17. public void insert(Thread thread){
  18. synchronized (object) {
  19. for(int i=0;i<100;i++){
  20. System.out.println(thread.getName()+"在插入数据"+i);
  21. arrayList.add(i);
  22. }
  23. }
  24. }
  25. }

上述代码就是synchronized代码块添加锁的两种方式,可以发现添加synchronized代码块,要比直接在方法上添加synchronized关键字更加灵活。

当我们用sychronized关键字修饰方法时,这个方法只能同时让一个线程访问,但是有时候很可能只有一部分代码需要同步,而这个时候使用sychronized关键字修饰的方法是做不到的,但是使用sychronized代码块就可以实现这个功能。

并且如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

来看一段代码:

  1. public class Test {
  2.  
  3. public static void main(String[] args)  {
  4. final InsertData insertData = new InsertData();
  5. new Thread(){
  6. public void run() {
  7. insertData.insert();
  8. }
  9.      }.start();
  10. new Thread(){
  11. public void run() {
  12. insertData.insert1();
  13. }
  14. }.start();
  15. }
  16. }
  17.  
  18. class InsertData {
  19. public synchronized void insert(){
  20. System.out.println("执行insert");
  21. try {
  22. Thread.sleep(5000);
  23. } catch (InterruptedException e) {
  24. e.printStackT\frace();
  25. }
  26. System.out.println("执行insert完毕");
  27. }
  28.  
  29. public synchronized static void insert1() {
  30. System.out.println("执行insert1");
  31. System.out.println("执行insert1完毕");
  32. }
  33. }

执行结果:

执行insert 执行insert1 执行insert1完毕 执行insert完毕

编程要求

请仔细阅读右侧代码,根据方法内的提示,在Begin - End区域内进行代码补充,具体任务如下:

  • 使num变量在同一时刻只能有一个线程可以访问。

测试说明

使程序的输出结果如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

 

开始你的任务吧,祝你成功!

package step2;




public class Task {




    public static void main(String[] args) {

       

        final insertData insert = new insertData();

       

        for (int i = 0; i < 3; i++) {

            new Thread(new Runnable() {

                public void run() {

                    insert.insert(Thread.currentThread());

                }

            }).start();

        }      

       

    }

}




class insertData{

   

    public static int num =0;

   

    /********* Begin *********/

    public synchronized void insert(Thread thread){

        //在方法声明中添加synchronized关键字

   

        for (int i = 0; i <= 5; i++) {

            num++;

            System.out.println(num);

        }

    }




/*方法二:在代码块中使用synchronized (object) 的形式

 public void insert(Thread thread){




 synchronized (this) { //this表示当前对象,也可以用其他对象作为锁*/

    /********* End *********/

}

 

posted @ 2023-05-18 19:39  梦羽儿  阅读(125)  评论(0编辑  收藏  举报