Java并发编程:马士兵并发笔记(一)线程同步基础

线程的创建

继承Thread类,并覆盖run()方法

Thread的实现类继承Thread类,并覆盖其run()方法,run()方法中定义线程需要执行的任务,并调用实现类的start()方法创建线程.

调用实现类的run()方法只是单纯的方法调用,并不能新建线程.

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("子线程启动,ID为:" + Thread.currentThread().getId() + ",名字为" + Thread.currentThread().getName());
    }
}

class Test {
    public static void main(String[] args)  {
        // 创建一个线程并开启线程
        MyThread thread = new MyThread();
        thread.start();
        // 多创建几个线程
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
    }
}

输出如下:

子线程启动,ID为:13名字为Thread-0
子线程启动,ID为:15名字为Thread-2
子线程启动,ID为:16名字为Thread-3
子线程启动,ID为:14名字为Thread-1

实现Runnable接口,并覆盖run()方法

因为Java是单继承的,因此直接继承Thread类常常并不是一个好主意.

我们常常通过实现Runnable接口或Callable接口定义一个任务,并将其传给Thread类构造函数来创建一个线程.其中Runnable类的run()方法没有返回值,而Callable类的call()方法可以有返回值.

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("子线程启动,ID为:" + Thread.currentThread().getId() + ",名字为" + Thread.currentThread().getName());
    }
}

class Test {
    public static void main(String[] args)  {
        // 通过将Runnable对象传入Thread构造函数来创建线程,并开启线程
        Runnable runnable = new MyRunnable();
        Thread thread1 = new Thread(runnable, "线程1");
        thread1.start();
        // 一个Runnable对象可以用来创建多个线程
        new Thread(runnable, "线程2").start();
        new Thread(runnable, "线程3").start();
        new Thread(runnable, "线程4").start();
    }
}

输出如下:

子线程启动,ID为:14,名字为线程2
子线程启动,ID为:13,名字为线程1
子线程启动,ID为:16,名字为线程4
子线程启动,ID为:15,名字为线程3

观察Thread类源代码,会发现Thread类也实现了Runnable接口.

实现Callable接口,并覆盖call()方法

通过该方法创建的任务可以有返回值.但Callable对象只能传给线程池,创建线程的具体方法见后边文章.

要注意CallableRunnable定义的都是任务而不是线程,要将其传入一个线程或线程池后才可以执行.

线程的状态

线程有五个状态:

新生(new)状态: 用new关键字建立一个线程对象后,该线程对象就处于新生状态,有自己的内存空间.

就绪(runnable)状态: 调用了start()方法后,线程转为就绪状态,处于就绪状态线程具备了运行条件,但还没分配到CPU,等待系统CPU调度.

运行(running)状态: 处于运行状态的线程正在执行自己的run()方法中代码.

阻塞(blocked)状态: 线程暂停执行,让出CPU并将其交给其他线程使用.

死亡(dead)状态: 当线程完成工作或抛出异常时,线程死亡,不再执行.

在这里插入图片描述

可以用厕所类比CPU来理解进程的五个状态:

  • 并不是你线程一start()就能办事了,要先排队等待CPU调度,此时处于就绪状态.
  • 当你的时间片用完后,就要出来重新排队,相当于受到CPU调度重新进入就绪状态.
  • 当出现一些原因导致线程运行不下去(等待给送手纸),就进入阻塞状态,直到解决阻塞后有重新排队等待CPU调度,此时处于就绪状态.

线程控制基本方法

判断线程状态的方法

  1. public long getId(): 得到线程的ID

  2. public String getName()public void setName(String name): 得到或设置线程名称

  3. public boolean isAlive(): 判断当前线程是否处于活动状态

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("run:" + this.isAlive());
    }
}

class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        System.out.println("begin:" + myThread.isAlive());
        myThread.start();
        System.out.println("end:" + myThread.isAlive());
    }
}
begin:false
run:true
end:true

我们看到程序输出end:true,说明再执行该句输出时,子线程还没运行完毕,实际上多运行几次程序就会发现,这个值是不确定的.

  1. public int getPriority()public void setPriority(int newPriority): 得到或设置线程优先级.

    Java中线程的优先级为1~10之间的整数,一个线程的默认优先级为5.

    Thread.MIN_PRIORITY = 1;
    Thread.MAX_PRIORITY = 10;
    Thread.NORM_PRIORITY = 5;

    优先级的高低只是意味着获得调度概率的高低,并不代表调度的绝对顺序.

  2. public static Thread currentThread()返回当前代码段正在被哪个线程调用.

阻塞线程的方法

  1. public void join(),public void join(long millis),public void join(long millis, int nanos): 合并线程,调用某线程的join()方法,会将当前线程与该线程合并,即等待该线程执行结束之后再恢复当前线程的运行.

    join()方法可以看作把并发的线程变为在一个线程内的函数调用

    class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
    			sleep(1000);
            }
        }
    }
    
    class Test{
        public static void main(String[] args) throws InterruptedException {
    		// 将子线程thread1 join进主线程,则主线程会等待子线程销毁之后再执行
            Thread thread1 = new MyThread();
            thread1.start();
            thread1.join();
            
    		// 当子线程thread1销毁之后,下边语句才会执行
            // 子线程thread2并没有join进主线程,会和主线程交替执行
            Thread thread2 = new MyThread();
            thread2.start();
            
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                Thread.sleep(1000);
            }
        }
    }
    

    程序输出如下:

    Thread-0:1
    Thread-0:2
    Thread-0:3
    Thread-0:4
    Thread-0:5
    Thread-1:1
    main:1
    main:2
    Thread-1:2
    Thread-1:3
    main:3
    main:4
    Thread-1:4
    main:5
    Thread-1:5
    
  2. public static sleep(long millis),

    public void sleep(long millis, int nanos): 使线程停止运行一段时间并转入阻塞状态.

    • sleep()方法不会交出锁.
    • 因为sleep()方法使线程进入阻塞状态,因此若调用了sleep()方法后,即使没有其他等待执行的线程,当前线程也不会马上恢复执行.
  3. public static void yield(): 礼让线程,让当前正在执行线程暂停并转入就绪状态.

    • yield()方法不会交出锁.
    • 因为yield()方法使线程进入就绪状态,因此若调用了yield()方法后,没有其他等待执行的线程,当前线程就会马上恢复执行.

线程同步

synchronized关键字

synchronized关键字的意义

  1. synchronized(对象)对括号内的对象加锁,任何线程要执行synchronized代码块中的代码,都必须要先拿到该对象的锁,当代码块执行完毕时,锁就会释放,被其他线程获取

    public class T {
    
        private int count = 10;
        private final Object lock = new Object();	// 锁对象
        
        public void m() {
            synchronized (lock) { // 任何线程要执行下面的代码,都必须先拿到lock锁,锁信息记录在堆内存对象中的,不是在栈引用中
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            }
            // 当上述synchronized代码块执行完毕后,锁就会被释放,然后被其他线程获取
        }   
    }
    
  2. 每次使用锁都新建一个锁对象比较麻烦,因此我们可以直接对this对象加锁.

    public class T {
    
        private int count = 10;
        
        public void m() {
            synchronized (this) { // 任何线程要执行下面的代码,必须先拿到this锁
                // synchronized锁定的不是代码块,而是this对象
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            }
        }
    }
    
  3. 若整个方法内所有代码都被synchronized修饰,则可以使synchronized关键字修饰整个方法.

    public class T {
    
        private int count = 10;
    
        public synchronized void m() { // 等同于 synchronized (this) { 
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
        // }
    }	
    
  4. synchronized关键字锁定静态方法,等价于锁定T.class对象

    public class T {
    
        private static int count = 10;
    
        public static synchronized void m() { // 等同于 synchronized (T.class) { 
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
        
        // 上边m()方法与下边mm()方法等价
        public static synchronized void mm() {
            synchronized (T.class) { 
                // 这里不能使用synchronized(this),因为静态方法不需要实例对象即可访问
            	count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            }
        }   
    }
    

synchronized关键字的使用

使用synchronized关键字修饰代码块,保证synchronized代码块内操作的原子性

public class T implements Runnable{

    private int count = 10;
    
    @Override
    public /*synchronized*/ void run() {
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

    public static void main(String[] args) {
        T t = new T();	 
        for (int i = 0; i < 5; i++) {
            new Thread(t).start();	//这里new的所有线程的锁住的是同一个上边的t对象
        }
    }
}

不加synchronized关键字,程序输出如下: 因为不保证原子性,每个线程在执行自减操作和输出操作之间都可能被其它线程打断.

Thread-0 count = 7
Thread-4 count = 5
Thread-3 count = 6
Thread-2 count = 7
Thread-1 count = 7

加上synchronized关键字,程序输出如下:

Thread-0 count = 9
Thread-4 count = 8
Thread-3 count = 7
Thread-2 count = 6
Thread-1 count = 5

细粒度的锁比粗粒度的锁效率高,因为锁的东西更少了。

深入理解synchronized关键字

  1. synchronized修饰的代码块中出现异常,线程进行异常处理后会马上释放锁(与ReentrantLock正相反).

    public class T {
    
        int i = 0;
    
        // 同步方法,计数到5抛出异常
        synchronized void m() {
            System.out.println(Thread.currentThread().getName() + " start");
            while (true) {
                i++;
                System.out.println(Thread.currentThread().getName() + ": " + i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    			
                // 计数到5抛出异常
                if (i == 5) {
                    int error = 1 / 0;
                }
            }
        }
    
    
        public static void main(String[] args) {
            T t = new T();
            new Thread(t::m, "线程1").start();
            new Thread(t::m, "线程2").start();
        }
    }
    
    

    输出如下,我们看到线程1抛出异常后马上释放锁,锁被线程2抢到并开始执行.

    线程1 start
    线程1: 1
    线程1: 2
    线程1: 3
    线程1: 4
    线程1: 5
    线程2 start
    Exception in thread "线程1" 
    java.lang.ArithmeticException: / by zero
    	at T.m(T.java:20)
    	at java.base/java.lang.Thread.run(Thread.java:835)
    线程2: 6
    线程2: 7
    线程2: 8
    线程2: 9
    线程2: 10
    线程2: 11
    

    webapp中,多个servlet访问同一个资源,如果异常处理不合适。在第一个线程中抛出异常,其他线程进入同步代码块,可能访问到第一个线程处理一半的数据,导致数据不一致。因此小心处理同步业务逻辑中的异常。

    如果不想释放锁——使用trycatch,捕获异常,进行处理

    try{
        // 计数到5抛出异常
        if (i == 5) {
            int error = 1 / 0;
        }
    }catch(Exception e){
        System.out.println("除0了");
    }
    
    线程1 start
    线程1: 1
    线程1: 2
    线程1: 3
    线程1: 4
    线程1: 50了
    线程1: 6
    线程1: 7
    线程1: 8
    线程1: 9
    
  2. synchronized锁住的是堆中o对象的实例,而不是o对象的引用,因为synchronized是针对堆中o对象的实例上进行计数.

    1. 若在程序运行过程中,引用o指向对象的属性发生改变,锁状态不变.
    2. 若在程序运行过程中,引用o指向的对象发生改变,则锁状态改变,原本抢到的锁作废,线程会去抢新锁.

    因此实际编程中常将锁对象的引用用final修饰,保证其指向的锁对象不发生改变.(final修饰引用时,该引用所指向的属性可以改变,但该引用不能再指向其他对象)

    public class T {
    
        Object o = new Object();
    
        // 该方法锁住的o对象引用没有被设为final
        void m() {
            synchronized (o) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "正在运行");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            T t = new T();
            new Thread(t::m, "线程1").start();
    
            // 在这里让程序睡一会儿,保证两个线程得到的o对象不同
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            Thread thread2 = new Thread(t::m, "线程2");
    
            // 改变锁引用,使得线程2也有机会运行,否则一直都是线程1运行
            t.o = new Object();
            thread2.start();
        }
    }
    

    程序输出如下,看到主线程睡了3秒之后,线程1线程2交替运行,他们各自抢到了不同的锁.

    线程1正在运行
    线程1正在运行
    线程1正在运行
    线程2正在运行
    线程1正在运行
    线程2正在运行
    线程1正在运行
    线程2正在运行
    线程1正在运行
    线程2正在运行
    ...
    

    如果没有改变锁引用,将会一直是线程1在运行。

  3. 不要以字符串常量作为锁定对象: 因为字符串常量池的存在,两个不同的字符串引用可能指向同一字符串对象

    public class T {
    
        // 两个字符串常量,作为两同步方法的锁
        String s1 = "Hello";
        String s2 = "Hello";
    
        // 同步m1方法以s1为锁
        void m1() {
            synchronized (s1) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + ":m1 is running");
                }
            }
        }
    
        // 同步m2方法以s2为锁
        void m2() {
            synchronized (s2) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + ":m1 is running");
                }
            }
        }
    
        public static void main(String[] args) {
            T t = new T();
    		
            // 输出两个锁的哈希码
            System.out.println(t.s1.hashCode());
            System.out.println(t.s2.hashCode());
    
            new Thread(t::m1, "线程1").start();
            new Thread(t::m2, "线程2").start();
        }
    }
    

    程序执行结果如下,我们发现两个字符串常量指向的是同一对象,且有一个线程永远得不到锁. 若我们的程序与某个库使用了同一个字符串对象作为锁,就会出现难以发现的bug.

    69609650
    69609650
    线程1:m1 is running
    线程1:m1 is running
    线程1:m1 is running
    线程1:m1 is running
    线程1:m1 is running
    线程1:m1 is running 
    
  4. synchronized方法和非synchronized方法是否可以同时执行?

    synchronized方法和非synchronized方法可以同时执行,因为非synchronized方法不需要抢这把锁。

    public class T {
    	// 同步方法
        public synchronized void m1() {
            System.out.println(Thread.currentThread().getName() + " m1 start");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " m1 end");
        }
    
        // 非同步方法
        public void m2() {
            System.out.println(Thread.currentThread().getName() + " m2 start");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " m2 end");
        }
    
        public static void main(String[] args) {
            T t = new T();
            new Thread(t::m1).start();
            new Thread(t::m2).start();
        }
    }
    

    程序输出如下:

    Thread-0 m1 start
    Thread-1 m2 start
    Thread-1 m2 end
    Thread-0 m1 end
    

    我们发现在同步方法m1()执行的同时,非同步方法m2()也在执行.

  5. synchronized是可重入锁,同一线程内同步方法之间可以相互调用

    1. 一个同步方法可以调用另外一个同步方法.若一个线程已抢到某对象的锁,再申请时仍然会得到该对象的锁. 因为这是在同一个线程以内,无非就是给锁上的数字加一.

      public class T {
      
          // 一个同步方法
          synchronized void m1() {
              System.out.println("m1 start");
              m2();	// 在同步方法m1()中调用同步方法m2(),不会发生死锁,因为这是在同一线程内的调用
              System.out.println("m1 end");
          }
      	
          // 另一个同步方法
          synchronized void m2() {
              System.out.println("m2 start");
              try {
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("m2 end");
          }
      
          public static void main(String[] args) {
              T t = new T();
              new Thread(t::m1).start();
          }
      }
      

      程序输出如下,没有发生死锁,且m1()方法会等待m2()方法结束后继续运行,说明这是函数调用,而非线程并行.

      m1 start 
      m2 start
      m2 end
      m1 end
      
    2. 同样的,子类的同步方法可以调用父类的同步方法也不会发生死锁,两个方法锁住的this指向的都是同一个子类对象.

      public class T {
      	// 父类同步方法
          synchronized void m2() {
              System.out.println("father method start");
              System.out.println("father method lock:" + this);
              try {
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("father method end");
          }
      }
      
      class TT extends T {
          // 子类同步方法
          @Override
          synchronized void m1() {
              System.out.println("child method start");
              System.out.println("child method lock:" + this);
              super.m();
              System.out.println("child method end");
          }
      
          public static void main(String[] args) {
              TT tt = new TT();
              new Thread(tt::m1).start();
          }
      }
      

      程序输出结果如下,没有发生死锁,且m1()方法会等待m2()方法结束后继续运行,说明这是函数调用,而非线程并行; 另外也可以看到父子的同步方法持有的是同一把锁。

      child method start
      child method lock:thread01.TT@2dd5c6ac
      father method start
      father method lock:thread01.TT@2dd5c6ac
      father method end
      child method end
      

死锁问题

多个进程在执行过程中互相等待对方的资源,导致阻塞

class Task1 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Task1 running");
            while (true) {
                synchronized (DeadLock.lock1) {
                    System.out.println("Task1 get lock1");
                    Thread.sleep(3000);     //获取lock1后此线程睡眠一会,给Lock2足够的时间获得lock2
                    synchronized (DeadLock.lock2) {
                        System.out.println("Task1 get lock2");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Task2 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Task2 running");
            while (true) {
                synchronized (DeadLock.lock2) {
                    System.out.println("Task2 get lock2");
                    Thread.sleep(3000);     //获取lock2后此线程睡眠一会,给Lock1足够的时间获得lock1
                    synchronized (DeadLock.lock1) {
                        System.out.println("Task2 get lock1");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class DeadLock {
    // 两个不同的锁对象
    public static Object lock1 = new Object();
    public static Object lock2 = new Object();

    public static void main(String[] args) {
        // 两个线程分别等待对方已经获得的锁
        Thread a = new Thread(new Task1());
        Thread b = new Thread(new Task2());
        a.start();
        b.start();
    }
}

出现死锁:

Task1 running
Task1 get lock1
Task2 running
Task2 get lock2

等待/通知(wait/notify)机制

wait()notify()方法

wait(),notify()notifyAll()方法是继承自Object的方法,都必须用在synchronized代码块中,( 调用被锁定对象的wait()、notify()方法)其作用如下:

  • public void wait(),public void wait(long timeoutMillis),public void wait(long timeoutMillis, int nanos): 使锁定在此对象上的线程暂停进入阻塞状态,wait()操作会释放锁.

    sleep不释放锁,wait释放锁,notify也不释放锁

  • public void notify(): 其他线程调用,唤醒一个正在wait在此对象上的线程.

  • public void notifyAll(): 唤醒正在wait在此对象上的所有进程.

其中wait()方法会释放锁,而notify()方法不会释放锁,而要等到synchronized代码块执行完才释放锁. 因此有时notify()方法唤醒其它线程后要再wait()一下释放锁,这样才有可能保证其它线程马上被唤醒.

在这里插入图片描述

解决方法——门闩CountdownLatch[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zJ5guXLk-1574498920194)(C:%5CUsers%5CDELL%5CDesktop%5C222)]

public class CountDownLatchDemo {
	
	volatile List lists = new ArrayList();
	
	public void add(Object o){
		lists.add(o);
	}
	
	public int size(){
		return lists.size();
	}
	
	public static void main(String[] args) {
		CountDownLatchDemo c = new CountDownLatchDemo();
		
		CountDownLatch latch = new CountDownLatch(1);//1变成0的时候门闩就开了
		
		new Thread(() -> {
			System.out.println("t2启动");
			if(c.size() != 5){
				try{
					latch.await();//等待门闩放下,不需要锁定任何对象
				}catch(Exception e){
					e.printStackTrace();
				}
			}
			System.out.println("t2结束");
		},"t2").start();
		
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		new Thread(() -> {
			System.out.println("t1启动");
			for(int i = 0;i < 10;i++){
				c.add(new Object());
				System.out.println("add "+i);
				
				if(c.size() == 5){
					latch.countDown();//打开门闩,可以继续向下运行,不需要释放锁
				}
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println("t1结束");
		},"t1").start();
	}
}

运行结果

t2启动
t1启动
add 0
add 1
add 2
add 3
add 4
t2结束
add 5
add 6
add 7
add 8
add 9
t1结束

生产者/消费者模式

两个线程使用同一容器,使用wait()notify()进行线程间通信的模式为生产者/消费者模式

// 产品类
class Product {
    int id;

    public Product(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Product{id=" + id + '}';
    }
}

// 同步栈,用于在生产者线程和消费者线程之间通信
class SyncStack {
    int index = 0;  // 栈顶元素上一位的下标

    Product[] products = new Product[4];

    // 向栈中送入产品,synchronized方法保证原子性
    public synchronized void push(Product product) {
        // 栈满了,停止生产
        while (index == products.length) {
            // 为防止wait时发生异常后执行程序剩余部分或栈被其它生产者线程生产满,使用while而非if检测栈状态
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 栈不满,通知消费者线程消费
        // 因为notify()方法不能指定唤醒哪个线程,若只唤醒了另一生产者线程,则会发生死锁,因此我们需要把所有线程都唤醒.
        this.notifyAll();
        products[index] = product;
        index++;
    }

    // 从栈中取出产品,synchronized保证原子性
    public synchronized Product pop() {
        // 栈空了,停止消费
        while (index == 0) {
            // 为防止wait时发生异常后执行程序剩余部分或栈被其它消费者线程消费空,使用while而非if检测栈状态
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 栈不空,通知生产者线程生产
        // 因为notify()方法不能指定唤醒哪个线程,若只唤醒了另一消费者线程,则会发生死锁,因此我们需要把所有线程都唤醒.
        this.notifyAll();
        index--;
        return products[index];
    }
}

// 生产者类
class Producer implements Runnable {
    // 生产者工作的栈
    SyncStack syncStack = null;

    public Producer(SyncStack syncStack) {
        this.syncStack = syncStack;
    }

    @Override
    public void run() {
        // 生产10个产品
        for (int i = 0; i < 10; i++) {
            Product product = new Product(i);
            syncStack.push(product);
            System.out.println("produce" + product);
            try {
                Thread.sleep((int)(Math.random()*200));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 消费者类
class Consumer implements Runnable {
    // 消费者消费的栈
    SyncStack syncStack = null;

    public Consumer(SyncStack syncStack) {
        this.syncStack = syncStack;
    }

    @Override
    public void run() {
        // 消费10个产品
        for (int i = 0; i < 10; i++) {
            Product product = syncStack.pop();
            System.out.println("consume" + product);
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ProducerConsumer {

    public static void main(String[] args) {
        SyncStack syncStack = new SyncStack();
        // 生产者和消费者都注入了同一个同步栈,线程间的锁都锁在同步栈syncStack上
        Producer producer = new Producer(syncStack);	// 生产者
        Consumer consumer = new Consumer(syncStack);	// 消费者
        // 创建多个生产者线程和消费者线程
        new Thread(producer, "生产者1").start();	// 生产者线程1	
        new Thread(producer, "生产者2").start();	// 生产者线程2
        new Thread(consumer, "消费者1").start();	// 消费者线程1
        new Thread(consumer, "消费者2").start();	// 消费者线程2
    }
}

通过wait/notify机制,我们可以保证生产者和消费者线程不会同时阻塞,消费者每次消费生产者每次生产都会唤醒对方线程.

在写生产者/消费者模式时,要注意下面几个问题:

  • 同步栈SyncStackpush()方法和pop()方法的操作均不具有原子性,因此我们需要通过加以synchronized修饰以保证线程同步。因为生产者和消费者注入的是同一SyncStack对象,因此两线程竞争的是同一把锁.

  • 一进入SyncStack类的push()方法和pop()方法,就要判断当前栈是否满/空,以确保数组不越界.判断栈满/空要使用while语句而非if语句. 有以下两个原因

    1. 在进程被唤醒期间,原本不满/不空的同步栈有可能被其它先被唤醒的生产者/消费者操作过后变满/变空.
    2. 一旦在wait()方法中出现异常,若使用if语句,则就会直接进入catch语句,打印异常并退出if语句,执行后面对数组的操作

    这两种情况下对数组进行操作都是危险的,因此我们要在线程被唤醒后仍然检查一次当前栈状况再操作栈.(根据Effective Java的说法,wait()方法在99.9%的情况下都是跟while语句在一起的).

  • 在唤醒其他线程时,我们使用notifyAll()方法而非notify()方法.这是因为我们无法指定环形的是哪个线程,若只唤醒了一个与本线程同角色(生产者唤醒生产者/消费者唤醒消费者)的线程,则会发生死锁,因此我们使用notifyAll()方法唤醒所有正在等待的线程.(根据Effective Java的说法,要永远使用notifyAll(),而不使用notify()).

volatile关键字

volatile关键字向编译器声明该变量是易变的,每次对volatile关键字的修改会通知给所有相关进程.

  1. 要理解volatile关键字的作用,要先理解Java内存模型JMM

    1. JMM中,所有对象以及信息都存放在主内存中(包含堆,栈),而每个线程在CPU中都有自己的独立空间,存储了需要用到的变量的副本.
    2. 线程对共享变量的操作,都会先在自己CPU中的工作内存中进行,然后再同步给主内存.若不加volatile关键字修饰,每个线程都有可能从自己CPU中的工作内存读取内存;而加以volatile关键字修饰后,每个线程对该变量进行修改后都会马上通知给所有进程.
    public class T {
    
        /*volatile*/ boolean running = true; 
        // 若无volatile关键字修饰,则变量running难以在每个线程之间共享,对running变量的修改自然不能终止线程
    
    	// 可以通过将running变量设为false来终止m()方法
        void m() {
            System.out.println("m start");
            while (running) { 
                // 死循环
            }
            System.out.println("m end");
        }
    
        public static void main(String[] args) {
            T t = new T();
            new Thread(t::m, "t1").start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    		
            // 将running变量设为false,观察线程是否被终止
            t.running = false;
        }
    }
    

    我们发现,若不对running变量加以volatile修饰,则对running变量的修改不能终止子线程,说明在主线程中对running的修改对子线程不可见.

    有趣的是,若在while死循环体中加入一些语句或sleep一段时间之后,可见性问题可能会消失,这是因为加入语句后,CPU就可能会出现空闲,并同步主内存中的内容到工作内存,但这是不确定的,因此在这种情况下还是尽量要加上volatile。

  2. volatile只能保证可见性,但不能保证原子性. volatile不能解决多个线程同时修改一个变量带来的线程安全问题.

    public class T {
    
        volatile int count = 0;
        /*AtomicInteger count = new AtomicInteger(0);*/
    
        /*synchronized*/ void m() {
            for (int i = 0; i < 10000; i++) {
                count++;
                /*count.incrementAndGet();*/
                //incrementAndGet()是原子方法,而count++不是原子方法
            }
        }
    
        public static void main(String[] args) {
            // 创建一个10个线程的容器,其中每个线程都执行m()方法
            T t = new T();
            List<Thread> threads = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(t::m, "t-" + i));
            }
    
            // 启动这10个线程并join到主线程,防止主线程先行结束
            for (Thread thread : threads) {
                try {
                    thread.start();
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            System.out.println(t.count);	// 10个线程,每个线程执行10000次,结果应为100000
        }
    }
    

    运行该程序,我们发现最终变量t.count并非如我们所预计的那样为100000,而是小于100000(当然,若去掉volatile修饰,最终t.count会更小).这说明volatile并不能保证对变量操作的原子性.

    要保证多线程操作同一变量的原子性,有如下两种方法:

    • 在方法上加synchronized修饰非原子操作的方法,synchronized既保证可见性,又保证原子性.但synchronized效率最低

    • 使用AtomicInteger代替int类型(AtomicXXX类可以用来替代基本数据类型,其支持一些原子操作)

      AtomicInteger count = new AtomicInteger(0);

      count.incrementAndGet();

      ps:单个Atomic操作是原子的,但是几句的组合是非原子的

      if(count.get() < 1000)

      count.incrementAndGet();

      比如这两句之间可能会有别的线程进来改变值,这种情况还是要加锁

    综上所述,volatile保证对被修饰变量的修改对于其他相关线程是可见的,即保证了可见性;但volatile并不能解决多个线程同时修改同一变量带来的线程安全问题,即不能保证原子性. 因此,只有在满足以下两个条件的情况下volatile才能保证解决线程的安全问题.

    1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
    2. 变量不需要与其他状态变量共同参与不变约束
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值