线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行同一段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
以电影院售票为例:
一个电影院在网页,APP和线下同时售票,一共有100张票。三个售票方式可以看作三个线程。
/*
* 多线程并发访问同一个数据资源
* 3个线程对一个票资源,出售
*/
public class ThreadDemo {
public static void main(String[] args) {
//创建Runnable接口实现类对象
Tickets t = new Tickets();
//创建3个Thread类对象,传递Runnable接口实现类
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t0.start();t1.start();t2.start();
}
}
public class Ticket implements Runnable {
//共100票
int ticket = 100;
@Override
public void run() {
//模拟卖票
while(true){
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
}
运行几次可以发现这种情况:
不同窗口卖出了同一张票,而且出现了错误的票0和-1。
这是因为某一线程在操作变量ticket时,进入了循环,但还没有完成打印操作该线程就被终止,下一线程开始执行,回到该线程时继续执行就会出现错误。
为了避免共享数据不同步问题,Java提供了线程同步机制(Synchronized)。
线程同步有两种方式:
- 同步代码块
- 同步方法
同步代码块
将可能产生同步问题的代码放入同步代码块中。
// 线程共享数据,保证安全,加入同步代码块
synchronized(obj) {
// 对票数判断,大于0,可以出售,--操作
if(ticket > 0) {
try {
Thread.sleep(10);
}catch(Exception ex) {}
System.out.println(Thread.currentThread().getName() + " 出售第" + ticket--);
}
}
obj
可以是任意对象,作为对象锁。线程进入同步代码块时将对象锁obj拿走,直到同步代码块执行完毕后将锁归还。其他线程只有在线程锁还在时才能进入同步代码块,这样就避免了发生不同步问题。
同步方法
/*
* 采用同步方法形式,解决线程的安全问题
* 好处:代码简洁
* 将线程共享数据和同步抽取到方法中
* 在方法的声明上加入同步关键字
*
* 问题:
* 同步方法有锁吗?
* 有,同步方法中的对象锁,是本类对象引用this
* 如果方法是静态的,同步有锁吗,是this吗?
* 有锁,但绝不能是this,锁是本类自己.class。
*/
public class Tickets implements Runnable {
// 定义可以出售的票源
private int ticket = 100;
public void run() {
while (true) {
payTicket();
}
}
public synchronized void payTicket() {
// 对票数判断,大于0,可以出售,--操作
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (Exception ex) {
}
System.out.println(Thread.currentThread().getName() + " 出售第" + ticket--);
}
}
}
代码简洁,同步方法的对象锁是本类对象自己this
,如果方法是静态的,那么锁是本类.class
。
Lock()接口
使用关键字synchronized
看不到对象锁的行为,Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
/*
* 使用JDK1.5的接口Lock替换同步代码块,替换同步代码块,实现线程 的安全性
* Lock接口方法
* lock()获取锁
* unlock()释放锁
*
*/
public class Tickets implements Runnable {
// 定义可以出售的票源
private int ticket = 100;
// 在类的成员位置,创建Lock接口的实现类对象
private Lock lock = new ReentrantLock();
public void run() {
while (true) {
// 调用Lock接口中的方法获取锁
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " 出售第" + ticket--);
} catch (Exception ex) {
}finally {
// 释放锁
lock.unlock();
}
}
}
}
}
死锁
同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
synchronzied(A锁){
synchronized(B锁){
}
}
// 测试代码
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock dead = new DeadLock();
Thread t0 = new Thread(dead);
Thread t1 = new Thread(dead);
t0.start();t1.start();
}
}
// 线程代码,同步嵌套,产生死锁
public class DeadLock implements Runnable{
private int i = 0;
public void run() {
while(true) {
if(i % 2 == 0) {
// 先进入A同步,再进入B同步
synchronized (LockA.locka) {
System.out.println("if...locka");
synchronized (LockB.lockb) {
System.out.println("if...lockb");
}
}
}else {
// 先进入B同步,在进入A同步
synchronized (LockB.lockb) {
System.out.println("else...lockb");
synchronized (LockA.locka) {
System.out.println("else...locka");
}
}
}
i++;
}
}
}
//两个对象锁类
public class LockA {
private LockA() {}
public final static LockA locka = new LockA();
}
public class LockB {
private LockB() {}
public final static LockB lockb = new LockB();
}
等待唤醒
在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制所涉及到的方法(以下均为Object类的方法):
- wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
- notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
- notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
下面是一个输入输出示例:
// 测试类
/*
* 开启输入输出线程,实现程序的赋值和打印值
*/
public class ThreadDemo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread tin = new Thread(in);
Thread tout = new Thread(out);
tin.start();tout.start();
}
/*
* 定义资源类,有2个成员变量
* name, sex
* 同时有两个线程对资源中的变量操作
* 1个对name,age赋值
* 1个对name,age做变量的输出打印
*/
public class Resource {
public String name;
public String sex;
public boolean flag = false;
}
/*
* 输入的线程,对资源对象Resource中成员变量赋值
* 一次赋值张三,男
* 下一次赋值李四,女
*/
public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
public void run() {
int i = 0;
while(true) {
synchronized (r) {
// 标记是true,等待
if(r.flag) {
try{
r.wait();
}catch(Exception ex) {
}
}
if(i % 2 == 0) {
r.name = "张三";
r.sex = "男";
}
else {
r.name = "lisi";
r.sex = "nv";
}
//将对方线程唤醒,flag改为true
r.flag = true;
r.notify();
}
i++;
}
}
}
/*
* 输出线程,对资源对象Resource中成员变量输出值
*/
public class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
public void run() {
while(true) {
synchronized (r) {
// 判断标记,是false,等待
if(!r.flag) {
try{
r.wait();
}catch(Exception ex) {
}
}
System.out.println(r.name + "..." + r.sex);
// 标记改成false,唤醒对方线程
r.flag = false;
r.notify();
}
}
}
}
输入输出的对象锁应该是同一个对象r,保证输入输出值的正确性。