首先我们上次在--java多线程基础--中说到,线程是独立的,可以是多个线程并行执行的并且不会影响其他线程的一条执行路径。
这个其实很让人误解,但是说是不会影响到其他线程也对,这里有必要解释一下多线程到底是为了什么?在干什么?
多线程是为了什么:多线程就是为了提高程序运行效率。
多线程在干什么:多线程就是在充分压榨CPU,让CPU不停的为我们工作,充分利用每一个线程来为我们做事情。
所以在--java多线程基础--所说的独立的不影响别的线程,指的是在运行期间,每个线程都是独立的,可以并行运行的一条执行路径。
那么进入正题:
首先第一个点,什么是线程安全?
答:我们上面也说到了,多线会充分的压榨UPC,来为我们做事情,现在硬件技术越来越好,我们CPU处理的速度也越来越快,当然大家知道什么太快了也不好~~咳咳,回到正题,线程安全问题,就是当多个线程同时共享同一个对象(全局变量,静态变量等),并且对该对象做操作的时候,那么就会发生线程安全问题
总结:线程安全就指的是,在一个线程操作该对象的过程中,没有其他线程对该对象做操作,通俗一点说,一次只能是一个线程对该对象做操作就是线程安全的,如果是操作过程中有其他线程也在操作该对象,那么就是线程不安全,也称之为线程安全问题,如果是多个线程进行读取该对象是不会发生线程安全问题,只有对该对象进行操作才会发生线程安全问题
我们举个多线程买火车票的例子:
//火车票
@Data
public class TrainTickets {
//表示我们有100个火车票
private static Integer number = 100;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try{
while (TrainTickets.number > 0){
Thread.sleep(50);
System.out.println(Thread.currentThread().getName()+"拿到了:"+TrainTickets.number--+" 张火车票");
}
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try{
while (TrainTickets.number > 0){
Thread.sleep(50);
System.out.println(Thread.currentThread().getName()+"拿到了:"+TrainTickets.number--+" 张火车票");
}
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
}
上面的例子就是,两个窗口同时抢火车票,也就是我们说的--操作--那么我们看看结果:
我们可以很清晰的看到我们的火车票卖重复了,这是不可取的,怎么可能存在两张相同火车票,而且后面的顺序也不对,我们希望看到的是一个降序的输出,因为现在硬件很好,其实我们要模拟线程安全问题还是比较难的,计算机处理速度现在普遍很快,我们可以借助Thread.sleep()进行让线程休眠,在同时执行,就可以看到线程安全问题。
那么这里就先基于Java并发包(java.util.concurrent)的三种解决方式:
首先我们需要知道什么是Java的内置锁:Java提供了一种内置的锁机制来支持原子性每一个Java对象都可以用作一个实现同步的锁,称为内置锁,就相当于一根网线,当一个电脑插着该网线的时候,别的网线就需要等待它使用完毕之后才能拿到网线进行使用
1:使用synchronized关键字(内置锁
1-1:同步代码块
//火车票
@Data
public class TrainTickets {
//表示我们有100个火车票
private static Integer number = 100;
//出票操作
public static Integer ticketIssue(){
synchronized (number){
return TrainTickets.number--;
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try{
while (TrainTickets.number >0) {
Thread.sleep(50);
System.out.println(Thread.currentThread().getName() + "拿到第了:" + TrainTickets.ticketIssue() + " 张火车票");
}
}catch (Exception e){
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try{
while (TrainTickets.number >0) {
Thread.sleep(50);
System.out.println(Thread.currentThread().getName() + "拿到第了:" + TrainTickets.ticketIssue() + " 张火车票");
}
}catch (Exception e){
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
}
我们对我们上次的代码稍加进行修改,新增出票方法,然后在对票操作的地方加上 synchronized
这次我们可以清晰的看到两个线程拿到的票是没有重复的,那么这就保证了我们的逻辑,从这里可以看出 synchronized 关键字是可以解决线程安全问题的。
下面对于synchrsyonized的使用介绍很关键
1-1-1:synchronized 关键字不仅仅可以是代码块,也可以加在方法上,加在方法上就代表整个方法都每次只能有一个线程进行执行,但是我们不推荐这样做,推荐使用同步代码块,在可能发生线程安全问题的地方使用同步代码块即可。
1-1-2:在同步代码块或者同步方法包裹的内容执行期间,我们可以理解为锁未释放,那么在这个期间的其他线程就算拿到了CPU的资源也是不可以进行运行,他们会进行等待,只有拿到了锁才可以进行运行,那么这样我们就保证了线程安全问题。
1-1-3:关于同步代码块的参数问题,可以使用任意对象,记住一定是对象,这里为什么说我上面可以使用Integer类型,因为Integer是int的封装对象,基本数据类型的封装对象都可以作为锁对象,还有String类型,当然还可以传递自己这个类对象,比如:synchrsyonized(TrainTickets.class),这就是使用this锁
1-1-4:死锁,什么是死锁:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。我们知道了一个线程只能拿到了锁资源才能进行执行,如果两个线程互相拿到了对方的钥匙,那么这个时候两个线程就卡在哪里不能执行,这是一个很严重的后果,所以我们要避免这个情况
1-1-5:如何避免死锁情况,那就是不要重复加锁,而且尽量使用同一把锁,也就是说不要在锁里面嵌套锁,而且就算在锁里面嵌套锁,也尽量使用同一个对象作为锁对象,其实说明白的,既然我们的同步代码块包裹的内容只有一个线程能执行,那么在里面继续枷锁的意义是啥,而且还容易出现死锁现象,因为当锁没有释放的时候其他线程拿不到锁,那么就永远执行不了,然而影响程序效率。
1-1-6:拓展 synchronized 修饰方法使用锁是当前this锁。synchronized 修饰静态方法使用锁是当前类的字节码文件
2:使用lock锁
private static Lock lock = new ReentrantLock();
//出票操作
public static Integer ticketIssue(){
Integer result = null;
try {
//上锁
lock.lock();
result = TrainTickets.number--;
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
return result;
}
这里我们使用lock锁进行实现synchronized的功能,我们还是使用TrainTickets这个类作为演示,main方法不变,我们只改变一下出票方法
运行结果:
从运行结果我们可以看到,lock锁可以以实现synchronized的功能
2-1-1:lock锁也遵循一把锁的规则,如果我们把锁创建在出票的方法中的话,那么同样会发生线程安全问题,因为每一个线程进来都是一个新的锁,所以我们还是要提供一把锁,而不是多把锁。
2-1-2:lock锁需要手动上锁,和释放锁,所以一般结合try-catch-finally进行使用,使用完之后必须释放锁
2-1-3:lock锁和synchronized的主要区别:那就是处理机制不同,而且lock锁可以进行条件判断并中断锁,因为在finally中我们释放了锁,那么其他锁就可以拿到锁资源进行运行。如果想详细了解二者区别请点击我:深入研究 Java Synchronize 和 Lock 的区别与用法
2-1-3:lock总结:需要手动上锁,解锁,可以在try中进行条件判断,如果发生某些情况可以抛出异常中断锁,让出资源。
到了这里基本上常用的java内置的锁,我们就讲解完毕了,他们都是给可能发生线程安全问题的地方加锁,保证一个线程进行运行,而保证线程安全问题,同样的他们还有一个名称,叫做重入锁,也就是他们的锁对象是可以传递的,也就是同一把锁,外层的锁可以进行传递,内层的锁可以同样拿到该锁对象。
3:ThreadLocal接口
如果想详细了解ThreadLocal的可以点击我:彻底理解ThreadLocal
这里我们直接说简单地说一下,ThreadLocal是怎么实现线程安全的,Threadlocal为每一个线程都创建了一个变量的副本,那么也就是说,每个线程访问的对象都是一个全新的,属于自己的对象,那么是属于自己一个人的,也就保证了只允许一个线程进行访问,那么只有一个线程进行访问,那么怎么会发生线程安全问题呢?至于他的原理,那就是使用的Map集合,把线程名称作为一个Key进行保存,下次如果存在key只需要取出保存的对象即可,如果可有该对象,那么就添加一个即可。
这是官方的一些源码,可以很清晰的看到,是保存在一个Map中。
threadLocal很简单,只有四个方法:
名称 | 作用 |
---|---|
initiaValue | 初始化数据使用 |
get | 得到保存的数据 |
set | 设置保存的数据 |
remove | 删除 |
那么我们还是使用示例,火车票进行使用ThreadLocal
public class MyThreadLocal implements Runnable {
private static Integer number = 100;
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return MyThreadLocal.number;
}
};
public Integer get(){
int number = threadLocal.get();
threadLocal.set(--number);
return number;
}
@Override
public void run() {
try{
while (threadLocal.get() >0) {
Thread.sleep(50);
System.out.println(Thread.currentThread().getName() + "拿到第了:" + get() + " 张火车票");
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThreadLocal myThreadLocal = new MyThreadLocal();
Thread thread = new Thread(myThreadLocal);
Thread thread1 = new Thread(myThreadLocal);
thread.start();
thread1.start();
}
}
上面就是使用Threadlocal的实例,
那么我们从运行图上面可以看到,Thread-0和Thread-1都是拿到了一百张火车票,而且顺序是对的,那这说明了什么,说明了两个线程没有共享一个对象,而是每个线程都有自己的专属的一个对象,所以才没有发生共同消费的情况,哪有人说不对啊,你这每张票都卖重复了,这是不和逻辑的啊,其实这只是为了演示这个效果,为了证明ThreadLocal中存在一份独立的对象副本,具体的使用场景需要根据情况而变,如果是希望很多线程共享一个数据,而又不发生重复西奥菲,那么就可以使用上面的加锁,如果是希望这个数据不能被别人修改,只能被自己使用,那么就可以使用ThreadLocal。
结语:到了这里就要和大家说再见了,本文适合小白学习,我也是一名小白,我希望可以和看到这篇文章的伙伴们共同进步,如果有哪些写的不对的,还希望大家指出,谢谢阅读~~