在多线程中,线程的执行是按照时间片划分的,从而线程的执行顺序具有随机性.当多个线程去访问共享的数据时,可能得到的结果和预期的结果不一致,产生线程安全问题.我们不能在提高系统效率的同时而忽略线程的安全问题,或者说,我们应该在保证线程安全的情况下,尽量的提升系统的效率.
一. 线程安全的判断
我们要判断一个程序是否是线程安全的,只需要看3个条件:
- 是不是多线程
- 是否存在共享资源
- 是否存在有多个线程同时访问共享资源
如果上述三个条件都存在,那么这个程序就存在线程安全的问题
二. 如何保证线程安全
保证线程安全的方式有很多,本文主要介绍加锁的方式,通过锁来保证线程安全.
将程序中共享的资源上锁,只有获得锁标记的线程才可以访问共享资源,其他需要访问资源但是没有获得锁的线程会阻塞到锁外,只有等到访问共享资源的线程访问完毕,释放锁,其他线程才可以去争夺锁,进而访问资源.
- synchronized (obj){}同步代码块
同步锁,在同一个时间只有一个线程可以访问上锁的资源.锁对象可以是任意的Object类型,将共享的代码包含在synchronized (obj){}的花括号内,这样括号内的内容就是一个不可分割的原子操作,实现线程安全
模拟电影院买票的实例,三个窗口负责卖100张电影票,明显存在线程安全问题,不解决线程安全,将会出现重票,票序紊乱.为了解决线程安全问题,我们将共享的代码上锁.如下:
package gw;
class MyRun implements Runnable {
Object obj = new Object();
private static int n = 100;
@Override
public void run() {
// 模拟电影院买票
while (n > 0) {
synchronized (obj) {
System.out.println(Thread.currentThread().getName()
+ "卖出第" + n-- + "张票");
}
}
}
}
public class Test6 {
public static void main(String[] args) {
// 创建任务对象
Runnable r = new MyRun();
// 将任务提交与3个线程
Thread t1 = new Thread(r, "窗口一");
Thread t2 = new Thread(r, "窗口二");
Thread t3 = new Thread(r, "窗口三");
// 启动3个线程,并发执行
t1.start();
t2.start();
t3.start();
}
}
- synchronized同步方法
当一个方法中的所有代码都是共享资源,我们就可以将这个方法设置为同步方法.
如下:将电影院买票的例子改为同步方法.效果和同步代码块一样.
package gw;
class MyRun2 implements Runnable {
private static int n = 100;
private synchronized void mon() {
// 模拟电影院买票
while (n > 0) {
System.out.println(Thread.currentThread().getName()
+ "卖出第" + n-- + "张票");
}
}
@Override
public void run() {
mon();
}
}
public class Test7 {
public static void main(String[] args) {
// 创建任务对象
Runnable r = new MyRun2();
// 将任务提交与3个线程
Thread t1 = new Thread(r, "窗口一");
Thread t2 = new Thread(r, "窗口二");
Thread t3 = new Thread(r, "窗口三");
// 启动3个线程,并发执行
t1.start();
t2.start();
t3.start();
}
}
- Lock接口与ReentrantLock
java.util.concurrent.locks 包下的Lock接口.
Lock接口提供了更为灵活的加锁方式,功能更加全面,缺点是需要手动释放锁 .
Lock接口的常用方法:
项目 | Value |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
boolean tryLock() | 尝试获得锁 |
lock()获得锁,与synchronized(obj){的功能一样,
而synchronized(obj){}在运行结束时会自动释放锁,但是lock()获得的锁需要使用unLock()方法释放锁
Lock多了一个常用的功能,tryLock(),尝试获得锁,在之前,当一个线程需要获得锁已经被其他线程获取时,当前线程就会阻塞自己,等待锁资源.而使用tryLock尝试获得锁时,如果没有获得锁,当前线程并不会阻塞自己,而是去做其他事情,等到一定时间再去尝试获得锁.
Lock是一个接口,不能直接实例化,我们通常使用其实现子类ReentrantLock实例化.
使用Lock接口加锁实例:
package gw;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyRun3 implements Runnable {
// 创建一个Lock对象.
Lock lock = new ReentrantLock();
private static int n = 100;
@Override
public void run() {
lock.lock();// 获得锁
// 模拟电影院买票
while (n > 0) {
System.out.println(Thread.currentThread().getName()
+ "卖出第" + (n--) + "张票");
}
lock.unlock();// 释放锁
}
}
public class Test8 {
public static void main(String[] args) {
// 创建任务对象
Runnable r = new MyRun3();
// 将任务提交与3个线程
Thread t1 = new Thread(r, "窗口一");
Thread t2 = new Thread(r, "窗口二");
Thread t3 = new Thread(r, "窗口三");
// 启动3个线程,并发执行
t1.start();
t2.start();
t3.start();
}
}
- ReadWriteLock接口与ReentrantReadWriteLock
java.util.concurrent.locks 包下的ReadWriteLock接口,将读锁与写锁设置不同的锁.
在学习读写锁之前,如果一个程序中的读操作远远大于写操作的话,为了线程安全又需要对其上锁,而读与读之间实际上是不会影响线程安全的,因此在这种情况下我们可以采取读写锁,来提高程序的运行效率.
我们通过下面的例子来说明读写锁的效率:
使用重入锁:
代码如下:
`package gw;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Student {
private String name;
// 创建重入互斥锁对象
Lock lock = new ReentrantLock();
// 写的方法,执行一次休眠1秒
public void setName(String name) {
lock.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
} finally {
lock.unlock();
}
}
// 读的方法,执行一次休眠1秒
public String getName() {
lock.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return name;
} finally {
lock.unlock();
}
}
}
public class Test9 {
public static void main(String[] args) {
System.out.println("程序开始!");
Student s = new Student();
Callable ca1 = new Callable() {
// 执行写的任务
@Override
public Object call() throws Exception {
s.setName("小白");
return null;
}
};
Callable ca2 = new Callable() {
// 执行读的任务
@Override
public Object call() throws Exception {
s.getName();
return null;
}
};
// 获取当前系统时间毫秒值
Long start = System.currentTimeMillis();
// 创建线程池
ExecutorService es = Executors.newFixedThreadPool(25);
// 提交写的任务3次
for (int i = 0; i < 3; i++) {
es.submit(ca1);
}
// 提交读的任务22次
for (int i = 0; i < 22; i++) {
es.submit(ca2);
}
es.shutdown();// 关闭线程池
// 判断线程池的线程是否已经结束
while (true) {
if (es.isTerminated()) {
break;
}
}
// 获取程序运行结束时间的毫秒值
Long end = System.currentTimeMillis();
System.out.println("程序结束,运行时间" + (end - start) + "毫秒");
}}
`
运行效果:
程序开始!
程序结束,运行时间25015毫秒
由于读写的方法都设置了休眠1秒,所以程序用时25秒左右.
实际上在读与读之间是可以并发执行的,我们采用读写锁来测试系统效率:
在java.util.concurrent.locks包下,有一个ReadWriteLock接口,这个接口提供了两个方法来获取读锁和写锁.
项目 | Value |
---|---|
public ReentrantReadWriteLock.ReadLock readLock() | 返回一个可重入的读锁 |
public ReentrantReadWriteLock.WriteLock writeLock() | 返回一个可重入的写锁 |
由于其返回类型是可重入的读写锁子实现类,所以我们直接使用其子类,ReentrantReadWriteLock创建一个可重入的读写锁:
**
- // 创建一个可重入读写锁
- ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
- ReadLock rl = rwl.readLock();// 获取读锁
- WriteLock wl = rwl.writeLock();// 获取写锁
package gwgw;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
class Student {
private String name;
// 创建重入互斥锁对象
// Lock lock = new ReentrantLock();
// 创建一个可重入读写锁
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
ReadLock rl = rwl.readLock();// 获取读锁
WriteLock wl = rwl.writeLock();// 获取写锁
// 写的方法,执行一次休眠1秒
public void setName(String name) {
wl.lock(); // 加写锁
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
} finally {
wl.unlock();// 释放锁
}
}
// 读的方法,执行一次休眠1秒
public String getName() {
rl.lock();// 加读锁
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return name;
} finally {
rl.unlock();// 释放锁
}
}
}
public class Test10 {
public static void main(String[] args) {
System.out.println("程序开始!");
Student s = new Student();
Callable ca1 = new Callable() {
// 执行写的任务
@Override
public Object call() throws Exception {
s.setName("小白");
return null;
}
};
Callable ca2 = new Callable() {
// 执行读的任务
@Override
public Object call() throws Exception {
s.getName();
return null;
}
};
// 获取当前系统时间毫秒值
Long start = System.currentTimeMillis();
// 创建线程池
ExecutorService es = Executors.newFixedThreadPool(25);
// 提交写的任务3次
for (int i = 0; i < 3; i++) {
es.submit(ca1);
}
// 提交读的任务22次
for (int i = 0; i < 22; i++) {
es.submit(ca2);
}
es.shutdown();// 关闭线程池
// 判断线程池的线程是否已经结束
while (true) {
if (es.isTerminated()) {
break;
}
}
// 获取程序运行结束时间的毫秒值
Long end = System.currentTimeMillis();
System.out.println("程序结束,运行时间" + (end - start) + "毫秒");
}
}
执行结果:
程序开始!
程序结束,运行时间4006毫秒
可见程序只用了4秒,3次写操作用时3秒,剩余的22次读操作并发执行,用时1秒,大大提高了系统效率.
三. 线程安全的集合
每一个线程不安全的集合会对应一个线程安全的版本
项目 | Value |
---|---|
ArrayList | CopyOnWriteArrayList |
Set | CopyOnWriteArraySet |
HashMap | ConcurrentHashMap |
Queue | ConcurentLinkedQueue |
阻塞Queue | ArrayBlockingAueue |