1. 线程安全
多线程访问共享数据才可能会发生线程安全
线程安全问题是不能产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了cpu
的执行权;让其他的线程只能等待。
代码:
public class RunnableImpl implements Runnable {
// 定义一个多线程共享票源
private int ticket = 100;
// 设置线程任务,卖票
@Override
public void run() {
while (true) {
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
主方法:(下面四个也要用到)
/*
模拟卖票案例
创建3个线程,同时开启,对共享的票进行出售
*/
public class Demo01Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象(因为要共享票源,所以只创建一个实现类,让三个线程进行卖票)
Runnable run = new RunnableImpl();
// System.out.println("run :" + run); // 同步方法块用到这句
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
结果:
Thread-2正在卖第:5票
Thread-0正在卖第:5票
Thread-1正在卖第:5票
Thread-2正在卖第:2票
Thread-0正在卖第:1票
Thread-1正在卖第:1票
Thread-2正在卖第:-1票
2. 线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:Java中提供了同步机制(synchronized)来解决。
有三种方式完成同步操作:
- 同步代码块。
- 同步方法。
- 锁机制。
3. 同步代码块
解决线程安全问题的第一种方案:使用同步代码块。卖票案例出现了线程安全问题. 卖出了不存在的票和重复的票。
原理
使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器。
格式
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
优缺点
-
保证了只能有一个线程在同步中执行共享程序 (执行完在把对象归还给同步锁,安全)
-
程序频繁的判断所、获取锁、释放锁,程序的效率会降低
注意
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须保证多个线程使用的锁对象是同一个
- 锁对象作用: 把同步代码块锁住,只让一个线程在同步代码块中执行
代码:
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
//创建一个锁对象(一个线程执行一次run方法,为了保证对象唯一,要放run方法外面)
Object obj = new Object();
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while(true){ // if不执行了,while也没终止,还在执行
//同步代码块
synchronized (obj){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
}
结果:
Thread-0-->正在卖第5张票
Thread-0-->正在卖第4张票
Thread-0-->正在卖第3张票
Thread-0-->正在卖第2张票
Thread-0-->正在卖第1张票
4. 同步方法
解决线程安全问题的二种方案:使用同步方法。 定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。
同步方法的锁对象是谁?
- 默认就是线程的实现类对象
new RunnableImpl()
,也是就是this
静态的同步方法锁对象是谁?
- 不能是
this
,this
是创建对象之后产生的,静态方法优先于对象。 - 静态方法的锁对象是本类的
class
属性:class
文件对象(反射)
格式
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
使用步骤
- 把访问了共享数据的代码抽取出来,放到一个方法中
- 在方法上添加synchronized修饰符
演示代码:
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private static int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
System.out.println("this:"+this);
//使用死循环,让卖票操作重复执行
while(true){
payTicket1();
// payTicket2();
// payTicketStatic();
}
}
// 1. 定义一个同步方法
public synchronized void payTicket1(){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
// 2. 验证锁对象是不是this
public /*synchronized*/ void payTicket2(){
synchronized (this){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
// 定义一个静态的同步方法
public static /*synchronized*/ void payTicketStatic(){
synchronized (RunnableImpl.class){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
结果:
run :cn.luis.demo10.Synchronized.RunnableImpl@27f674d
this:cn.luis.demo10.Synchronized.RunnableImpl@27f674d
this:cn.luis.demo10.Synchronized.RunnableImpl@27f674d
this:cn.luis.demo10.Synchronized.RunnableImpl@27f674d
Thread-0-->正在卖第100张票
Thread-0-->正在卖第99张票
Thread-2-->正在卖第98张票
Thread-2-->正在卖第97张票
Thread-1-->正在卖第96张票
Thread-1-->正在卖第95张票
5. Lock锁
解决线程安全问题的三种方案:使用Lock锁
java.util.concurrent.locks.Lock
接口。Lock 实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法
void lock()
:获取锁。void unlock()
:释放锁。
实现类ReentrantLock
java.util.concurrent.locks.ReentrantLock implements Lock
使用步骤
- 在成员位置创建一个
ReentrantLock
对象 - 在可能会出现安全问题的代码前调用Lock接口中的方法
lock
获取锁 - 在可能会出现安全问题的代码后调用Lock接口中的方法
unlock
释放锁
代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
// 在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
while (true) { // if不执行了,while也没终止,还在执行
// 代码优化
l.lock();
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
l.unlock(); // 无论程序是否异常,都会把锁释放掉
}
}
}
}
}