Appearance
JUC 多线程安全问题
本文介绍在Java多线程开发过程中会出现的安全问题。
1. 多线程问题
现有如下代码:count初始化为0,线程t1对count执行5000次自增操作,线程t2对count执行5000次自减操作,线程t1和t2运行结束后,count的值应该为0。
java
public class MultiThreadTest04 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 =
new Thread("t1") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
count ++;
}
}
};
t1.start();
Thread t2 = new Thread("t2"){
@Override
public void run() {
for (int i = 0;i<5000;i++){
count --;
}
}
};
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}但是,实际执行这段代码,count的结果并不总是为0,有时会出现正数或者负数,这就是多线程的问题所在。
Java中对静态变量的自增、自减操作并不是原子操作,从字节码角度分析,自增自减操作各自包含四条指令:
txt
getstatic i #获取静态变量i
iconst_1 #准备常量1
iadd #自增
putstatic i #将修改后的值存入静态变量i复制复制失败复制成功txt
getstatic i #获取静态变量i
iconst_1 #准备常量1
isub #自减
putstatic i #将修改后的值存入静态变量i复制复制失败复制成功而Java的内存模型如下,完成静态变量的自增、自减需要在主存和工作内存之间进行数据交换:

如果是单线程,以上八行字节码指令是顺序执行的,则不会出错:

但是,如果是多线程,由于线程切换的原因,以上八行字节码可能交错运行,错误就会出现:
出现i=-1的情况:

出现i=1的情况:

2. 临界区与竞态条件
在同一程序中运行多个线程本身不会导致问题,如果多个线程访问了相同的资源(如内存区、文件等),则可能会导致问题。
- 如果多个线程只是对共享资源进行读取,不会出现问题;
- 如果多个线程对共享资源进行读写,则会出现问题。
**临界区:**一段代码对共享资源进行多线程读写,则该段代码称为临界区;
**竞态条件:**多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,则发生了竞态条件。
3. 线程安全分析
上节阐述了多个线程对共享资源进行读写才会发生安全问题,本小节按情况说明。
3.1 静态变量
- 如果多线程没有共享静态成员变量,则不会引发线程安全问题;
- 如果多线程共享了静态成员变量,则分为两种情况:
- 多线程只有读取操作,不会引发线程安全问题;
- 多线程有读写操作,则会产生竞态条件,会引发线程安全问题;
例如,现有静态变量count=0,线程t1操作count自增5000次,线程t2操作count自减5000次,则count最后应该为0。
java
public class MultiThreadTest04 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 =
new Thread("t1") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
count ++;
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
for (int i = 0;i<5000;i++){
count --;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}结果:

count最终结果不为0,出现了线程安全问题。
3.2 成员变量
同静态成员变量相同,只有多线程读写同一对象的成员变量,才会引发线程安全问题。
java
public class MultiThreadTest05 {
public static void main(String[] args) throws InterruptedException {
Number number = new Number();
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
number.add();
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
number.sub();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(number.getCount());
}
}
class Number{
private int count = 0;
public void add(){
count++;
}
public void sub(){
count--;
}
public int getCount(){
return count;
}
}结果为:

3.3 局部变量
如果多线程共享同一个局部变量,则分情况讨论:
- 如果局部变量是基本类型,如
int,double等,则是线程安全的; - 如果局部变量是引用类型:
- 如果引用的对象没有逃离方法作用范围,则是线程安全的;
- 如果引用的对象逃离了方法作用范围,则不是线程安全的。
例如,现有静态方法test1():
java
public static void test1(){
int i = 10;
i++;
}现有两个线程,分别调用方法test1(),则线程内存模型如下,在两个线程中,分别存在test1栈帧,所有局部变量i分别存在于两个线程的栈帧中,互不干涉。

如果是局部变量是引用类型,则需要看其他线程是否能访问到局部引用变量所指向的对象,如果能,则线程不安全,否则线程安全,即是否发生了逃逸。
4. 解决安全问题-synchronized
4.1 synchronized介绍
synchronized,即俗称的对象锁,它采用互斥的方式让同一时刻至多一个线程能持有对象锁,其他线程再想获取这个对象锁时就会被阻塞,这样就能保证拥有锁的线程可以安全地执行临界区代码,不用担心线程上下文切换所带来的线程安全性问题。
synchronized的语法规则如下:
java
synchronized(对象){
临界区代码
}要保证多个线程是准备持有同一个对象锁的。
如果synchronized加在成员方法上,则对象锁为该类的一个实例对象:
java
class Test{
public synchronized void test(){
...
}
}
//等价于
class Test{
public void test(){
synchronized(this){
...
}
}
}如果synchronized加在静态方法上,则对象锁为该类的class对象:
java
class Test{
public synchronized static void test(){
...
}
}
//等价于
class Test{
public void test(){
synchronized(Test.class){
...
}
}
}4.2 案例问题解决
使用synchronized解决之前的问题:
java
public class MultiThreadTest04 {
private static int count = 0;
private static final Object LOCK = new Object(); //锁对象
public static void main(String[] args) throws InterruptedException {
Thread t1 =
new Thread("t1") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
synchronized (LOCK) { //加锁
count++;
}
}
}
};
Thread t2 =
new Thread("t2") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
synchronized (LOCK) { //加锁
count--;
}
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}最终的结果count始终为0。
4.3 synchronized使用案例
通过几个案例,来判断synchronized锁住的是哪个对象以及结果。
4.3.1 情形一
java
public class Synchronized8_1 {
public static void main(String[] args) {
Test test = new Test();
new Thread(()->{test.a();}).start(); //线程一
new Thread(()->{test.b();}).start(); //线程二
}
}
class Test{
public synchronized void a(){
System.out.println("aaaa");
}
public synchronized void b(){
System.out.println("bbbb");
}
}锁住的对象是实例test,打印结果为:
txt
aaaa
bbbb
-----------
bbbb
aaaa由于是线程一先启动,所有第二种情况很少出现。
4.3.2 情形二
java
public class Synchronized8_2 {
public static void main(String[] args) {
Test test = new Test();
new Thread(() -> {test.a();}).start(); //线程一
new Thread(() -> {test.b();}).start(); //线程二
new Thread(() -> {test.c();}).start(); //线程三
}
}
class Test {
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("aaaa");
}
public synchronized void b() {
System.out.println("bbbb");
}
public void c() {
System.out.println("cccc");
}
}只有线程一和线程二有互斥关系,其要竞争持有的锁对象为实例test,线程三所调用的方法并没有synchronized,所以线程三与另外两个线程没有互斥关系。
打印结果有三种:
txt
cccc
一秒后
aaaa
bbbb
--------------
cccc
bbbb
一秒后
aaaa
--------------
bbbb
cccc
一秒后
aaaa因为线程一在打印aaaa之前,会先睡眠1秒钟,并且线程三并不需要获取锁就能执行,所以cccc始终会在aaaa之前打印,所以结果不可能出现aaaa先打印再打印cccc。
4.3.3 情形三
java
public class Synchronized8_3 {
public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();
new Thread(() -> {test1.a();}).start(); //线程一
new Thread(() -> {test2.b();}).start(); //线程二
}
}
class Test {
public synchronized void a() {
System.out.println("aaaa");
}
public synchronized void b() {
System.out.println("bbbb");
}
}线程一持有的锁对象是test1,线程二持有的锁对象是test2,所以两个线程没有互斥关系,打印结果:
txt
aaaa
bbbb
---------------
bbbb
aaaa4.3.4 情形四
java
public class Synchronized8_3 {
public static void main(String[] args) {
Test test1 = new Test();
new Thread(() -> {test1.a();}).start(); //线程一
new Thread(() -> {test1.b();}).start(); //线程二
}
}
class Test {
public static synchronized void a() { //静态方法
System.out.println("aaaa");
}
public synchronized void b() {
System.out.println("bbbb");
}
}线程一持有的锁对象是Test.class,线程二持有的锁对象是test1,两个线程并没有互斥关系,所以打印结果:
txt
aaaa
bbbb
--------------
bbbb
aaaa4.3.5 情形五
java
public class Synchronized8_3 {
public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();
new Thread(() -> {test1.a();}).start(); //线程一
new Thread(() -> {test2.b();}).start(); //线程二
}
}
class Test {
public static synchronized void a() { //静态方法
System.out.println("aaaa");
}
public static synchronized void b() { //静态方法
System.out.println("bbbb");
}
}线程一和线程二竞争的是同一个锁对象Test.class,所以两个线程是互斥关系,打印结果为取决于哪个线程先拿到对象锁:
txt
aaaa
bbbb
---------------
bbbb
aaaa5. 练习题
5.1 卖票
现有一个售票窗口,有余票10000张,有2000人来买票,每个人买票数在1-5张不等,请用程序模拟。
java
public class TicketSale {
public static void main(String[] args) throws InterruptedException {
TicketWindow ticketWindow = new TicketWindow(10000); //售票窗口,余票10000
List<Thread> threads = new LinkedList<>(); //线程列表,模拟顾客
List<Integer> counts = new Vector<>(); //存储每个顾客买的票数
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(){
@Override
public void run() {
//模拟买票
int amount = ticketWindow.sale(((new Random()).nextInt(5))+1);
counts.add(amount); // 存储该位顾客买的票数
}
};
threads.add(thread);
thread.start();
}
// 等待2000位顾客买票结束
for (Thread thread : threads) {
thread.join();
}
// 统计卖出去的票和剩余的票
int count = 0;
for (Integer i : counts) {
count += i;
}
System.out.println("卖出去的票为:"+count);
System.out.println("剩余的票为:"+ticketWindow.getTickets());
System.out.println("卖出去的票和剩余的票总共为:"+(ticketWindow.getTickets()+count));
}
}
class TicketWindow{
private int tickets; //余票数
public TicketWindow(int tickets){
this.tickets = tickets;
}
public int getTickets(){
return tickets;
}
// 售票,返回成功的售票数
public int sale(int amount){
if (tickets >= amount){
tickets -= amount;
return amount;
}else {
return 0;
}
}
}多运行几次,结果为:


我们发现卖出去的票和剩余的票之和超过了原定的余票数(超卖问题),所以该方法存在线程安全问题。
我们需要将买票的过程加锁,由于只有一个售票窗口,所以在方法sale()上加关键字synchronized即可:
java
public synchronized int sale(int amount){
if (tickets >= amount){
tickets -= amount;
return amount;
}else {
return 0;
}
}为什么List<Integer> counts = new Vector<>();不改用List<Integer> counts = new LinkedList<>();呢?
查看Vector类的add()方法源码:
java
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}查看LinkedList类的add()方法源码:
java
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}所以Vector是线程安全的,LinkedList是线程不安全的,所以也不需要将counts.add(amount);上锁。
5.2 转账
现有两个银行账户accountA和accountB,余额均为2000元,两个账户分别向另一个账户转账2000次,每次转账的金额在1-100元不等,请用程序模拟:
java
public class BankAccount {
public static void main(String[] args) throws InterruptedException {
Account accountA = new Account(2000); // 账户A
Account accountB = new Account(2000); // 账户B
Thread thread1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
// 账户A向账户B转账
accountA.transfer(accountB, ((new Random()).nextInt(100)) + 1);
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
// 账户B向账户A转账
accountB.transfer(accountA, ((new Random()).nextInt(100)) + 1);
}
}
};
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("账户A和账户B余额之和为:"+(accountA.getAccount()+accountB.getAccount()));
}
}
class Account {
private int account; // 账户余额
public Account(int account) {
this.account = account;
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
// 转账
public void transfer(Account target, int amount) {
if (this.account >= amount) {
this.setAccount(this.getAccount() - amount);
target.setAccount(target.getAccount() + amount);
}
}
}运行结果:

发现两个账户余额之和不是4000,导致转账后余额变少,说明出现了线程安全问题。
我们需要将转账过程置为临界区,那锁对象应该如何设呢?
java
public synchronized void transfer(Account target, int amount) {
if (this.account >= amount) {
this.setAccount(this.getAccount() - amount);
target.setAccount(target.getAccount() + amount);
}
}这样设置可行吗?不可以,如果这样设置,则线程一持有锁对象为accountA,线程二持有锁对象为accountB,两个线程不是互斥关系。
应该让两个线程竞争同一个对象锁,所以可以考虑如下设置:
java
public void transfer(Account target, int amount) {
synchronized (Account.class) {
if (this.account >= amount) {
this.setAccount(this.getAccount() - amount);
target.setAccount(target.getAccount() + amount);
}
}
}运行结果: