前言
终于要来到最后一部分了,多线程。我们之前的程序基本都是单线程的,可能在可视化写监听的时候用到了多线程,但是其实多线程是我们提升程序运行效率的重要手段,但它也带来了很多麻烦的问题,这一章就来简单的总结一下。
并发
Processes:进程,私有空间,彼此隔离。
Threads:线程 ,程序内部的控制机制。
进程之间不共享内存,线程却可以,这就是麻烦的开始但也是高效的起步。
如果说进程是虚拟机的话,线程就是虚拟机的cpu。
那如何创建多线程呢?
通常有两种方法:
- 从Thread类中派生出子类,实现run方法。
- 从Runnable接口中构造Thread对象。
再使用.start()启动线程,并不是.run(),.run()只是一个方法并不是启动线程。
下面有个简单的例子
//使用Runnable创建并启动一个线程
new Thread(new Runnable(){
@Override
public void run(){}
}).start();
//使用Thread创建线程
class myThread extends Thread{
@Override
public void run(){}
}
new mythread.start();
Thread thread = new myThread();
thread.start();
交错和竞争
多线程,程序是如何做到多线程的呢?
使用时间分片,通过时间分片,程序可以在多个线程之间来回 切换,一会执行这个,一会又执行另一个,下面这张图更容易理解:
时间分片是由OS自动调整的,所以我们没办法完全掌控目前正在运行的线程是什么,而多线程必然会有竞争,大家都抢着被执行,而线程之间又是共享内存的,那就是数据共享呗,害得抢数据,这就很容易出问题。我们知道代码语言是人写的,电脑想看懂得转成机器语言,那么我们一行代码,比如如下代码中的a=a+1,先不说机器语言,就说汇编语言它都被分成几步,先得寄存器指向成员变量的地址,然后再进行运算,再写回,所以别看代码才一行,但执行起来可能得好几步。
int a = 0 ;
a = a+1;
分成好几步就有问题了,在线程执行这几步之间,就容易被插队,比如我在这个线程里刚拿到a的值了,但是被时间分片打住了,另一个线程也进行同样的操作并且顺利完成,那a的值变了啊,被打住的线程恢复后是不会在重新回去取值的,而是接着执行的,所以这就导致a的值少了一次加一。这就有bug了,关键是这种bug还贼难受,你不知道OS啥时候切到这个线程,啥时候切回去,所以可能每一次bug出现的原因都不一样,这就让我们debug变得无比的困难。
线程安全
上面讲了产生bug的原因,那我们就得解决啊。
- Confinement限制共享:既然我因为共享数据出问题,好吧,那就不共享数据咯,每个线程只能用自己的数据,但这样就极大的限制了多线程的高效性。
- Immutability不可变:不可变数据通常都是线程安全的,为啥呢,因为它不可变啊,只能读,不能写,那我就能避免因为写操作带来的bug了,但这样也限制了一些比较好用的数据类型。
- 使用线程安全的数据类型:我们可以将mutable的数据类型转换成线程安全的数据类型,比如最常用的集合类collection,可以使用Collections.synchronizedMap/List/Set(new Collection())来将线程不安全的集合类转换成线程安全的,这个方法的原理就是把数据类型中的方法都变成原子的,啥叫原子执行呢,就是我每次都执行完一个完整的语句,这一个完整的语句就是原子执行的,这条语句在执行的时候是不能被其他线程打断的,所有这样就极大程度保证线程在使用mutable数据的方法是不被切掉。但这并不是万能的,我上面所它能使得一个方法是原子执行的,但不能保证方法与方法之间的执行也是原子的啊,比如使用迭代器iterator进行迭代或隐式迭代的时候,它都不是线程安全的,还有就是一旦线程多了,改有的竞争还得有。
- 那就更绝一点,我扩大原子执行的范围,我让一整块代码都是原子执行的,这就上锁了。java提供一种锁的机制使得我们能锁住一块代码,使得它在执行的时候是原子执行的----Synchronization。比如下面这行代码
Object lock = new Object();
Synchronized(lock){
int a = 0 ;
a = a + 1 ;
}
我用lock给里面a的两行代码上锁,那么线程在执行的时候就是把整个锁住的代码都是原子执行的了,不能被打扰,也不能插队,如果其他线程想执行这段代码就必须得排队,等当前线程执行完后才有可能切进来执行。这个方法在很大程度上就约束了线程的执行,所以它是有副作用的,本来把我线程(多核的情况,单核就没意思了嗷)可以同时执行这些代码的,但限制我得排队执行,那多捞啊,多核变单核,这就降低我们多线程的高效性。
前三种策略的核心思想:避免共享–>即使共享,也只能读不能写–>即使能读能写,共享的可变数据应自己具备在多线程之间协调的能力
缺陷也很明显:不能使用全局的rep–>只能读不能写–>可以共享读写,但只有单一方法是安全的,多方法不安全。
那对于锁这种机制呢,就有可能出现死锁的情况,比如下面这个例子:
线程T1拿到了锁a,T2拿到了锁b,但都没执行完不会释放锁,那互相卡死,这就是死锁的情况。所以我们在设计锁结构的时候,也要注意考虑会不会出现死锁的情况。