线程安全
1、线程安全是什么?
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,这就是线程安全问题
2、举例说明
电影院买票,假设电影院作为总共100个
3、代码实现卖票的例子
// 使用Runnable接口创建新的线程
public class RunnableImpl implements Runnable {
// 定义了一个多线程的共享数据
private int ticket = 100;
// 创建一个锁对象
Object obj = new Object();
// 设置线程任务:卖票
@Override
public void run() {
// 让卖票操作重复执行
while(true){
// 先判断票是否存在
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"-->正在卖第" +ticket+"张票"
}
}
}
}
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
最终的到的结果会出现,重复的票和不存在的票
线程同步
1、同步代码块
线程安全问题的一种解决方式就是使用同步代码块
同步代码块的格式
synchronized(锁对象) {
可能会出现线程安全问题的代码块(访问了共享数据的代码)
}
注意事项
① 通过代码块中的锁对象,可以使用任意的对象
② 必须保证多个线程使用的锁对象是同一个
③ 锁的作用:把同步代码块锁住,只让一个线程在同步代码块中执行
举例说明
// 使用Runnable接口创建新的线程
public class RunnableImpl implements Runnable {
// 定义了一个多线程的共享数据
private int ticket = 100;
// 创建一个锁对象
Object obj = new Object();
// 设置线程任务:卖票
@Override
public void run() {
// 让卖票操作重复执行
while(true){
// 使用同步代码块
synchronized(obj){
// 先判断票是否存在
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"-->正在卖第" +ticket+"张票"
}
}
}
}
}
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
同步技术的原理:
使用到了锁对象,这个锁对象叫同步锁,也叫做对象锁,也叫做对象监视器
由上述代码我们可以看出来,3个进程一起抢夺cpu的执行权,谁抢到了谁就执行run方法进行卖票操作。
当t0抢到了cpu的执行权,它就执行run方法,遇到synchronized代码块这时t0会检查synchronized代码块是否有锁对象,发现有锁对象就会获取这个锁对象,把它带走,然后进入到同步代码块中执行接下来的操作。
当t1抢到了cpu的执行权,它就执行run方法,遇到synchronized代码块这时t1会检查synchronized代码块是否有锁对象,发现没有锁对象,那么t1就会进入到阻塞状态,会一致等待t0线程执行完同步代码块中的代码后把锁对象归还给同步代码块,然后t1才能获取到锁对象,进入同步代码块中执行操作
总结:
同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步代码块
2、同步方法
线程安全问题的另一种解决方式就是使用同步方法
使用步骤
① 把访问了共享数据的代码块抽取出来,放到一个方法当中
② 在方法上添加上synchronized修饰符
定义格式
修饰符 synchronized 返回值 方法名(参数列表) {
可能会出现线程安全问题的代码块(访问了共享数据的代码)
}
举例说明
// 使用Runnable接口创建新的线程
public class RunnableImpl implements Runnable {
// 定义了一个多线程的共享数据
private int ticket = 100;
// 设置线程任务:卖票
@Override
public void run() {
// 让卖票操作重复执行
while(true){
payTicket();
}
}
}
// 定义一个同步方法
public synchronized void payTicket(){
// 先判断票是否存在
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"-->正在卖第" +ticket+"张票"
}
}
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
同步方法的原理:
同步方法的原理和同步代码块一样,把需要访问共享数据的代码锁住,只让一个线程执行。
那么它的锁是谁呢?同步方法的锁是this,也就是实现类对象
3、静态同步方法:
静态同步方法也能解决线程安全问题
静态同步方法的锁对象是本类的class属性也就是class文件对象
4、Lock锁
Lock锁也是解决线程安全问题的方式之一
使用步骤
① 在成员位置创建一个ReentrantLock对象(因为Lock锁是一个接口,ReentrantLock是Lock接口的实现类之一)
② 在可能出现线程安全问题的代码前调用Lock中的lock()方法获取锁
③ 在可能会出现线程安全问题的代码后调用Lock中的unlock()方法用来释放锁(归还锁)
举例说明
// 使用Runnable接口创建新的线程
public class RunnableImpl implements Runnable {
// 定义了一个多线程的共享数据
private int ticket = 100;
// 创建一个ReentrantLock对象
Lock l = new ReentrantLock();
// 设置线程任务:卖票
@Override
public void run() {
// 让卖票操作重复执行
while(true){
l.lock();
// 先判断票是否存在
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"-->正在卖第" +ticket+"张票"
}
l.unlock();
}
}
}
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
也可以在final代码块中使用unlock()方法,这样可以保证无论是否程序出现异常都会释放锁