全网最详细并发编程(2)—入门篇

1. 共享模型之管程

本节内容
 ● 共享问题
 ● synchronized
 ● 线程安全分析
 ● Monitor
 ● wait/notify
 ● 线程状态转换
 ● 活跃性
 ● Lock

1.1 共享带来的问题

全网最详细并发编程(2)---入门篇_线程安全
全网最详细并发编程(2)---入门篇_线程安全_02
全网最详细并发编程(2)---入门篇_线程安全_03全网最详细并发编程(2)---入门篇_并发编程_04
全网最详细并发编程(2)---入门篇_d3_05
全网最详细并发编程(2)---入门篇_java_06
全网最详细并发编程(2)---入门篇_线程安全_07
全网最详细并发编程(2)---入门篇_d3_08
全网最详细并发编程(2)---入门篇_线程安全_09

static int counter = 0;
static void increment() 
// 临界区
{ 
 counter++; }
static void decrement() 
// 临界区
{ 
 counter--; }

全网最详细并发编程(2)---入门篇_java_10
全网最详细并发编程(2)---入门篇_多线程_11
全网最详细并发编程(2)---入门篇_java_12
全网最详细并发编程(2)---入门篇_多线程_13
全网最详细并发编程(2)---入门篇_线程安全_14
全网最详细并发编程(2)---入门篇_并发编程_15全网最详细并发编程(2)---入门篇_线程安全_16
面向对象改进
把需要保护的共享变量放入一个类

class Room {
 int value = 0;
 public void increment() {
	 synchronized (this) {
		 value++;
	 }
	 }
 public void decrement() {
	 synchronized (this) {
	 	value--;
	 }
 }
 public int get() {
	 synchronized (this) {
	 	return value;
	 }
 }
}
@Slf4j
public class Test1 {
 
 public static void main(String[] args) throws InterruptedException {
	 Room room = new Room();
	 Thread t1 = new Thread(() -> {
	 for (int j = 0; j < 5000; j++) {
	 	room.increment()
	  }
	 }, "t1");
	 Thread t2 = new Thread(() -> {
	 for (int j = 0; j < 5000; j++) {
	 	room.decrement();
	 }
	 }, "t2");
	 t1.start();
	 t2.start();
	 t1.join();
	 t2.join();
	 log.debug("count: {}" , room.get());
 }
}

1.3 方法上的 synchronized

	class Test{
		 public synchronized void test() {
	 
	 	}
	}
	//等价于
	class Test{
	 	public void test() {
	 		synchronized(this) {  //锁住this对象(成员变量)
	 		}
	 	}
	}

	class Test{
	 	public synchronized static void test() {
	 }
	}
	//等价于
	class Test{
	 	public static void test() {
			 synchronized(Test.class) {//锁住Test.class类对象(静态变量)
	 		}
	 	}
	}

不加 synchronized 的方法
 不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

所谓的“线程八锁”
其实就是考察 synchronized 锁住的是哪个对象
情况1:锁住同一个对象都是this(n1对象),结果为:1,2或者2,1

@Slf4j(topic = "c.Number")
class Number{
 	public synchronized void a() {
 		log.debug("1");
 }
	 public synchronized void b() {
 		log.debug("2");
 }
}
public static void main(String[] args) {
 	Number n1 = new Number();
 	new Thread(()->{ n1.a(); }).start();
	new Thread(()->{ n1.b(); }).start();
}

情况2:锁住同一个对象都是this(n1对象),结果为:1s后1,2或者2,1s后1

@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
 	sleep(1);
 	log.debug("1");
 }
 public synchronized void b() {
 	log.debug("2");
 }
}
public static void main(String[] args) {
	 Number n1 = new Number();
	 new Thread(()->{ n1.a(); }).start();
	 new Thread(()->{ n1.b(); }).start();
}

情况3:a,b锁住同一个对象都是this(n1对象),c没有上锁。结果为:3,1s后1,2或者3,2,1s后1或者2,3,1s后1

@Slf4j(topic = "c.Number")
class Number{
 	 public synchronized void a() {
	 	sleep(1);
	 	log.debug("1");
 }
	 public synchronized void b() {
	 	log.debug("2");
	 }
	 public void c() {
	 	log.debug("3");
	 }
	}
	public static void main(String[] args) {
		 Number n1 = new Number();
		 new Thread(()->{ n1.a(); }).start();
		 new Thread(()->{ n1.b(); }).start();
		 new Thread(()->{ n1.c(); }).start();
}

情况4:a锁住对象this(n1对象),b锁住对象this(n2对象),不互斥。结果为:2,1s后1

@Slf4j(topic = "c.Number")
class Number{
	 public synchronized void a() {
		 sleep(1);
		 log.debug("1");
	 }
	 public synchronized void b() {
	 	log.debug("2");
	 }
	}
	public static void main(String[] args) {
		 Number n1 = new Number();
		 Number n2 = new Number();
		 new Thread(()->{ n1.a(); }).start();
		 new Thread(()->{ n2.b(); }).start();
}

情况5:a锁住Number.class类对象,b锁住对象this(n1对象),不互斥。结果为:2,1s后1

@Slf4j(topic = "c.Number")
class Number{
	 public static synchronized void a() {
		 sleep(1);
		 log.debug("1");
	 }
	 public synchronized void b() {
	 	log.debug("2");
	 }
	}
	public static void main(String[] args) {
		 Number n1 = new Number();
		 new Thread(()->{ n1.a(); }).start();
		 new Thread(()->{ n1.b(); }).start();
}

情况6:a,b锁住都是Number.class类对象,互斥。结果为:2,1s后1或者1s后1,2

@Slf4j(topic = "c.Number")
class Number{
	 public static synchronized void a() {
		 sleep(1);
		 log.debug("1");
	 }
	 public static synchronized void b() {
	 	log.debug("2");
	 }
	}
	public static void main(String[] args) {
		 Number n1 = new Number();
		 new Thread(()->{ n1.a(); }).start();
		 new Thread(()->{ n1.b(); }).start();
}

情况7:a锁住Number.class类对象,b锁住对象this(n1对象),不互斥。结果为:2,1s后1

@Slf4j(topic = "c.Number")
class Number{
	 public static synchronized void a() {
		 sleep(1);
		 log.debug("1");
	 }
	 public synchronized void b() {
	 	log.debug("2");
	 }
	}
	public static void main(String[] args) {
		 Number n1 = new Number();
		 Number n2 = new Number();
		 new Thread(()->{ n1.a(); }).start();
		 new Thread(()->{ n2.b(); }).start();
}

情况8:a,b都锁住Number.class类对象,互斥。结果为:2,1s后1或者1s后1,2

class Number{
	 public static synchronized void a() {
		 sleep(1);
		 log.debug("1");
	 }
	 public static synchronized void b() {
	 	log.debug("2");
	 }
	}
	public static void main(String[] args) {
		 Number n1 = new Number();
		 Number n2 = new Number();
		 new Thread(()->{ n1.a(); }).start();
		 new Thread(()->{ n2.b(); }).start();
}

1.4 变量的线程安全分析

成员变量和静态变量是否线程安全?

 ● 如果它们没有共享,则线程安全
 ● 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
  ○如果只有读操作,则线程安全
  ○如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

 ● 局部变量是线程安全的
 ● 但局部变量引用的对象则未必
  ○如果该对象没有逃离方法的作用访问,它是线程安全的
  ○如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

public static void test1() {
 int i = 10;
 i++; }

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

public static void test1();
 descriptor: ()V
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
	 stack=1, locals=1, args_size=0
	 0: bipush 10
	 2: istore_0
	 3: iinc 0, 1
	 6: return
	 LineNumberTable:
	 line 10: 0
	 line 11: 3
	 line 12: 6
	 LocalVariableTable:
	 Start Length Slot Name Signature
	 3 4 0 i I

全网最详细并发编程(2)---入门篇_多线程_17
局部变量的引用稍有不同

先看一个成员变量的例子

class ThreadUnsafe {
	 ArrayList<String> list = new ArrayList<>(); //成员变量
	 public void method1(int loopNumber) {
		 for (int i = 0; i < loopNumber; i++) {
			 // { 临界区, 会产生竞态条件
			 method2();
			 method3();
			 // } 临界区
		 }
 }
	 private void method2() {
	 	list.add("1");
	 }
	 private void method3() {
	 	list.remove(0);
	 }
}

执行:

static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
	 ThreadUnsafe test = new ThreadUnsafe();
	 for (int i = 0; i < THREAD_NUMBER; i++) {
	 	new Thread(() -> {
	 		test.method1(LOOP_NUMBER);
			 }, "Thread" + i).start();
	 }
}

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
 at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 at java.util.ArrayList.remove(ArrayList.java:496) 
 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748)

分析:
 ● 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
 ● method3 与 method2 分析相同
全网最详细并发编程(2)---入门篇_d3_18
将 list 修改为局部变量

class ThreadSafe {
 public final void method1(int loopNumber) {
	 ArrayList<String> list = new ArrayList<>();//局部变量
	 for (int i = 0; i < loopNumber; i++) {
		 method2(list);
		 method3(list);
 }
 }
 private void method2(ArrayList<String> list) {
 	list.add("1");
 }
 private void method3(ArrayList<String> list) {
 	list.remove(0);
 }
}

那么就不会有上述问题了
全网最详细并发编程(2)---入门篇_java_19
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
 ● 情况1:有其它线程调用 method2 和 method3
 ● 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法。会有线程安全问题。

class ThreadSafe {
	 public final void method1(int loopNumber) {
		 ArrayList<String> list = new ArrayList<>();
		 for (int i = 0; i < loopNumber; i++) {
		 method2(list);
		 method3(list);
	 }
	 }
	 private void method2(ArrayList<String> list) {
	 	list.add("1");
	  }
	 private void method3(ArrayList<String> list) {
	 	list.remove(0);
	 }
	}
	class ThreadSafeSubClass extends ThreadSafe{
	 @Override
		 public void method3(ArrayList<String> list) {
			 new Thread(() -> {
			 	list.remove(0);
		 		}).start();
	 }
}

全网最详细并发编程(2)---入门篇_并发编程_20
常见线程安全类
 ● String
 ● Integer
 ● StringBuffer
 ● Random
 ● Vector
 ● Hashtable
 ● java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

Hashtable table = new Hashtable();
new Thread(()->{
 table.put("key", "value1");
}).start();
new Thread(()->{
 table.put("key", "value2");
}).start();

 ● 它们的每个方法是原子的
 ● 但注意它们多个方法的组合不是原子的,见后面分析

线程安全类方法的组合
分析下面代码是否线程安全?

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 	table.put("key", value);
}

全网最详细并发编程(2)---入门篇_并发编程_21

不可变类线程安全性
  String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。或许你有疑问,String 有 replace,substring (创建了一个新的对象)等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

实例分析
例1:Servlet运行在Tomcat环境下并只有一个实例,因此会被Tomcat的多个线程共享使用,因此存在成员变量的共享问题。

public class MyServlet extends HttpServlet {
	 // 是否安全?  否:HashMap不是线程安全的,HashTable是
	 Map<String,Object> map = new HashMap<>();
	 // 是否安全?  是:String 为不可变类,线程安全
	 String S1 = "...";
	 // 是否安全? 是
	 final String S2 = "...";
	 // 是否安全? 否:不是常见的线程安全类
	 Date D1 = new Date();
	 // 是否安全?  否:引用值D2不可变,但是日期里面的其它属性比如年月日可变。与字符串的最大区别是Date里面的属性可变。
	 final Date D2 = new Date();
 
	 public void doGet(HttpServletRequest request, 		HttpServletResponse response) {
	  // 使用上述变量
	 }
}

例2:

public class MyServlet extends HttpServlet {
	 // 是否安全?  否:Servlet只有一份,userService为其成员变量也只有一份,所以会有多个线程共享使用。
	 private UserService userService = new UserServiceImpl();
	 
	 public void doGet(HttpServletRequest request, HttpServletResponse response) {
	 	userService.update(...);
	 }
	}
	public class UserServiceImpl implements UserService {
		 // 记录调用次数   否:成员变量只有一份
		 private int count = 0;
		 
		 public void update() {
			 // ...  临界区
			 count++;
	 }
}

例3:

@Aspect
@Component
public class MyAspect {
	 // 是否安全?  否:Spring中没有加Scope默认为单例,因此为成员变量MyAspect会被共享,其成员变量也会被共享。解决:使用环绕通知,将开始时间和结束时间作为环绕通知的局部变量。
	 private long start = 0L;
	 
	 @Before("execution(* *(..))")
	 public void before() {
	 	start = System.nanoTime();
	 }
	 
	 @After("execution(* *(..))")
	 public void after() {
		 long end = System.nanoTime();
		 System.out.println("cost time:" + (end-start));
	 }
}

例4:

public class MyServlet extends HttpServlet {
		 // 是否安全    是:UserService不可变,没有地方修改它
		 private UserService userService = new UserServiceImpl();
		 
		 public void doGet(HttpServletRequest request, HttpServletResponse response) {
		 	userService.update(...);
		 }
	}
	public class UserServiceImpl implements UserService {
		 // 是否安全     是:Dao不可变
		 private UserDao userDao = new UserDaoImpl();
		 
		 public void update() {
		 userDao.update();
		 }
	}
	public class UserDaoImpl implements UserDao { 
		 // 是否安全   是:没有成员变量,无法修改其状态和属性
		 public void update() {
		 	String sql = "update user set password = ? where username = ?";
		 	// 是否安全   是:不同线程创建的conn各不相同,都在各自的栈内存中
		 	try (Connection conn = DriverManager.getConnection("","","")){
		 	// ...
		 	} catch (Exception e) {
		 	// ...
		 	}
		 }
}

例5:

public class MyServlet extends HttpServlet {
	 // 是否安全
	 private UserService userService = new UserServiceImpl();
	 public void doGet(HttpServletRequest request, HttpServletResponse response) {
	 	userService.update(...);
	 }
	}
	
	public class UserServiceImpl implements UserService {
	 	// 是否安全
		 private UserDao userDao = new UserDaoImpl();
		 
		 public void update() {
		 userDao.update();
		 }
}

	 public class UserDaoImpl implements UserDao {
		 // 是否安全    否:conn为成员变量被多个线程共享
		 private Connection conn = null;
		 public void update() throws SQLException {
		 String sql = "update user set password = ? where username = ?";
		 conn = DriverManager.getConnection("","","");
		 // ...
		 conn.close();
		 }
		}

例6:

public class MyServlet extends HttpServlet {
	 // 是否安全   是
	 private UserService userService = new UserServiceImpl();
	 public void doGet(HttpServletRequest request, HttpServletResponse response) {
	 	userService.update(...);
	 }
	}
	
	public class UserServiceImpl implements UserService { 
	 public void update() {
	 //是否安全:   是:因为userDao为局部变量,每个线程创建的都不一样(但不推荐,建议把conn作为线程内的局部变量)
	 UserDao userDao = new UserDaoImpl();
	 	userDao.update();
	 }
	}
	
	public class UserDaoImpl implements UserDao {
	 // 是否安全  
	 private Connection = null;
	 public void update() throws SQLException {
	 String sql = "update user set password = ? where username = ?";
	 conn = DriverManager.getConnection("","","");
	 // ...
	 conn.close();
	 }
}

例7:

public abstract class Test {
	 public void bar() {
	 // 是否安全  否:虽然sdf 是局部变量,但是还要看是否暴露给其它线程
	 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	 foo(sdf);
	}
 	public abstract foo(SimpleDateFormat sdf);
 	public static void main(String[] args) {
 		new Test().bar();
 }
}

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法。不想往外暴露方法设置为final或者private,String类加final就是防止子类将其属性或者状态覆盖。

public void foo(SimpleDateFormat sdf) {
	 String dateStr = "1999-10-11 00:00:00";
	 for (int i = 0; i < 20; i++) {
		 new Thread(() -> {
		 try {
		 	sdf.parse(dateStr);
		 } catch (ParseException e) {
		 	e.printStackTrace();
		 }
		 }).start();
	 }
}

例8:

private static Integer i = 0;
	public static void main(String[] args) throws InterruptedException {
	 List<Thread> list = new ArrayList<>();
	 for (int j = 0; j < 2; j++) {
		 Thread thread = new Thread(() -> {
		 for (int k = 0; k < 5000; k++) {
		 	synchronized (i) {
		 	i++;
		 }
		 }
	 }, "" + j);
	 list.add(thread);
	 }
	 list.stream().forEach(t -> t.start());
	 list.stream().forEach(t -> {
		 try {
		 	t.join();
		 } catch (InterruptedException e) {
		 	e.printStackTrace();
		 }
 });
  	log.debug("{}", i);
}

1.4习题

卖票练习
测试下面代码是否存在线程安全问题,并尝试改正

package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // 模拟多人买票
        TicketWindow window = new TicketWindow(1000);

        // 所有线程的集合(由于threadList在主线程中,不被共享,因此使用ArrayList不会出现线程安全问题)
        List<Thread> threadList = new ArrayList<>();
        // 卖出的票数统计(Vector为线程安全类)
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                // 买票
                int amount = window.sell(random(5));
                // 统计买票数
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }

        for (Thread thread : threadList) {
            thread.join();
        }

        // 统计卖出的票数和剩余票数
        log.debug("余票:{}",window.getCount());
        log.debug("卖出的票数:{}", amountList.stream().mapToInt(i -> i).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int random(int amount) {
        return random.nextInt(amount) + 1;
    }
}

// 售票窗口
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    // 获取余票数量
    public int getCount() {
        return count;
    }

    // 售票
    public synchronized int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

转账练习
测试下面代码是否存在线程安全问题,并尝试改正

package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

// 账户
class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    // 转账
    public void transfer(Account target, int amount) {
        synchronized(Account.class) {   //锁住Account类,因为涉及到A.money和B.money。
            if (this.money >= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

1.5 Monitor 概念

Java 对象头
以 32 位虚拟机为例
全网最详细并发编程(2)---入门篇_线程安全_22
全网最详细并发编程(2)---入门篇_d3_23
全网最详细并发编程(2)---入门篇_并发编程_24全网最详细并发编程(2)---入门篇_d3_25
全网最详细并发编程(2)---入门篇_线程安全_26
全网最详细并发编程(2)---入门篇_多线程_27
全网最详细并发编程(2)---入门篇_java_28全网最详细并发编程(2)---入门篇_d3_29全网最详细并发编程(2)---入门篇_多线程_30全网最详细并发编程(2)---入门篇_线程安全_31全网最详细并发编程(2)---入门篇_d3_32全网最详细并发编程(2)---入门篇_d3_33
锁膨胀
全网最详细并发编程(2)---入门篇_线程安全_34
全网最详细并发编程(2)---入门篇_多线程_35
自旋优化
全网最详细并发编程(2)---入门篇_线程安全_36
全网最详细并发编程(2)---入门篇_多线程_37
偏向锁
全网最详细并发编程(2)---入门篇_线程安全_38
全网最详细并发编程(2)---入门篇_d3_39
偏向状态
全网最详细并发编程(2)---入门篇_多线程_40
全网最详细并发编程(2)---入门篇_d3_41
全网最详细并发编程(2)---入门篇_d3_42
全网最详细并发编程(2)---入门篇_并发编程_43
全网最详细并发编程(2)---入门篇_java_44
全网最详细并发编程(2)---入门篇_多线程_45
撤销偏向锁
优先状态:偏向锁–>轻量级锁–>重量级锁
全网最详细并发编程(2)---入门篇_并发编程_46
全网最详细并发编程(2)---入门篇_线程安全_47
全网最详细并发编程(2)---入门篇_线程安全_48
全网最详细并发编程(2)---入门篇_d3_49
偏向锁-批量重偏向
全网最详细并发编程(2)---入门篇_d3_50
全网最详细并发编程(2)---入门篇_并发编程_51
全网最详细并发编程(2)---入门篇_并发编程_52
偏向锁-批量撤销
全网最详细并发编程(2)---入门篇_d3_53
全网最详细并发编程(2)---入门篇_d3_54
全网最详细并发编程(2)---入门篇_java_55
锁消除
全网最详细并发编程(2)---入门篇_线程安全_56
全网最详细并发编程(2)---入门篇_java_57


欢迎关注公众号Java技术大联盟,会不定期分享BAT面试资料等福利。

全网最详细并发编程(2)---入门篇_多线程_58