全网最详细并发编程(2)—入门篇
1. 共享模型之管程
本节内容
● 共享问题
● synchronized
● 线程安全分析
● Monitor
● wait/notify
● 线程状态转换
● 活跃性
● Lock
1.1 共享带来的问题
static int counter = 0;
static void increment()
// 临界区
{
counter++; }
static void decrement()
// 临界区
{
counter--; }
面向对象改进
把需要保护的共享变量放入一个类
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
局部变量的引用稍有不同
先看一个成员变量的例子
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 分析相同
将 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);
}
}
那么就不会有上述问题了
方法访问修饰符带来的思考,如果把 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();
}
}
常见线程安全类
● 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);
}
不可变类线程安全性
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 位虚拟机为例
锁膨胀
自旋优化
偏向锁
偏向状态
撤销偏向锁
优先状态:偏向锁–>轻量级锁–>重量级锁
偏向锁-批量重偏向
偏向锁-批量撤销
锁消除
欢迎关注公众号Java技术大联盟,会不定期分享BAT面试资料等福利。