1.认识多线程
2.如何去创建多线程并且启动
3.多线程的属性和方法
4.线程在执行时会出现的状态
5.多线程带来的风险(为什么会出现线程不安全的情况)
(一)认识多线程
1)线程是什么:
在学习多线程之前,我们通常写的都是进程,把一个进程当作一个“执行流”,但是在学习多线程后,会发现线程也是⼀个"执⾏流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间同时执行着多份代码。
2)为什么我们需要线程:
1.因为我们的cpu资源是有限的,而一个进程会占用一整个cpu核心的资源,所以我们通过线程,并发编程的来更合理运用cpu核心资源
2.虽然多线程也能并发编程,但是线程更加的轻量,他的创建,销毁,调度都比进程更快更便捷,消耗的资源也更少。
但是现在我们仍然嫌线程的频繁创建和销毁开销较大,所以有了协程(也就是轻量级的线程),线程池的出现。
3)线程和进程的关系:
首先我们要明确进程是包含线程的,每一个进程中都必须要有一个主线程存在
其次要知道线程是调度执行的最小单位,进程是系统分配资源的最小单位。进程与进程间不共用同一块资源,而同一个进程中的线程共用这个进程所申请的空间
进程与进程之间的关系不大,一个进程挂了另一个进程照样可以执行,而同一个进程中的一个线程挂了,可能会导致整个进程挂掉。
(二)如何去创建多线程并且启动
创建多线程的方法有很多比如
1.继承thread重写run方法
public class text {
private static class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.printf("这是一个线程");
}
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread();
MyThread myThread2=new MyThread();
myThread.start();
myThread2.start();
}
}
这里的每一个线程都是一个执行流,在之前,一个进程中如果有一个无限循环的while那么就不会执行到之后的方法,而用多线程是并发执行的,并不会影响其他线程。
2.实现Runnable接口并且重写run方法
public class text {
private static class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.printf("这是一个线程");
}
}
}
public static void main(String[] args) {
Thread t1=new Thread(new MyRunnable());
t1.start();
}
}
创建Thread类实例,调⽤Thread的构造⽅法时将1Runnable对象作为target参数.
对比上面两种方法:
• 继承Thread类,直接使用this就表示当前线程对象的引用.
• 实现Runnable接,this表示的是MyRunnable 的引用.需要使用 Thread.currentThread()
3.我们可以使用匿名内部类的方式来创建线程(此处省略)
4.lambda表达式
public class text {
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.printf("这也是个线程");
});
t1.start();
}
}
这里我们需要注意lambda表达式会涉及到一个叫变量捕获的东西,就是在lambda表达式里的变量必须是用final修饰的(不可变的)或者没有发生改变的量-------实时final
在说完如何创建多线程后有个问题,之前我在网上看到有人问run 和 start 的区别,在我看来这两个没有什么联系。如果我们在main方法中调用了重写后的run方法,run方法里的方法体能够被执行出来,但是不是以多线程的方式执行,只有调用了start方法,我们才真正创建了一个线程。
我们用jconsole可以看到在我们调用run方法是显示的是new,而在调用start时是runnable,那我们接下来就要说了什么是new,什么是runnable了----线程执行时的状态
(三)多线程的属性和方法
多线程的几个常见属性
在我们创建了一个线程之后,系统会给我们线程自动分配一个id和name,尤其是id,id是每个线程所独有的,不同线程之间不会重复,而名字我们可以手动做修改,在jconsole中我们可以看到线程的名字,当然也包含其他属性。
状态我们等会说,先说优先级,目前我没有发现这个优先级有什么用,看网上的文献说,优先级高的线程更容易被cpu调度执行,这里我还是要打上一个问号,因为多线程是随即调度抢占式执行的。
后台线程指的是,如果这个线程没有结束也不会影响进程的结束,反之只要有一个非后台线程没有结束,这个进程就不会结束。
是否存活也可以很简单的理解为,这个线程的run方法是否执行完了。
中断线程(我认为中断线程有一些难度,所以在这里着重写一下):
我们有两个方式可以中断线程
1.设定一个标志位,我们可以在外面修改
2.使用interrupt()方法来通知
示例1 通过设定标志位来中断线程(标志位要用volatile修饰)此关键字之后说
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了⼤事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
target.isQuit = true;
}
}
示例2使用Thread.interrupted或者 Thread.currenthread.interrupted来代替自定义标志位
public class text {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while(!Thread.interrupted())
System.out.printf("sos!!");
});
t1.start();
Thread.sleep(1000);
System.out.printf("我来救你了");
t1.interrupt();
}
}
thread收到通知的方式有两种
1)被wait/sleep/join等方法阻塞挂起等待,需要我们用异常的形式通知来清楚这个中断标志,当出现这个异常以后,线程不会直接中断而是要看catch中的代码,可以忽略这个异常,也可以直接结束线程。
2)
否则,只是内部的⼀个中断标志被设置,thread可以通过
◦Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志这种方式通知收到的更及时,即使线程正在sleep也可以马上收到。
使用join()方法等待
简单来说join方法我们通常用来使一个线程等待另一个线程执行完毕
同时join方法中会有不同的参数如下图:
第一个无参join方法我个人不推荐使用,因为如果另一个线程一直不结束,这个线程就会一直等待,我们也称为死等
第二个带有一个参数哥join方法是带有超时时间的等,一旦等待线程等了超过这个时间,就不等了,仍然执行自己之后的代码。默认的单位是毫秒。第三个方法则是第二个join方法的细分,可以支持更高的精度。
获取当前线程的引用
public class text {
public static void main(String[] args) {
Thread t1=Thread.currentThread();
System.out.println(t1.getId());
}
}
使用sleep()休眠当前线程
sleep是Thread中给我们提供的方法,而且sleep会抛出一个异常,如果我们想要中断线程,可以在抛出异常并且catch后进行处理
很多人会把sleep会wait弄混,等之后讲到线程安全时,我们会着重来讲一下他们两个的区别。
(四)线程在执行时会出现的状态
首先我们要明确,线程的状态是枚举类型的Thread.State
线程的六大状态
• NEW:安排了⼯作,还未开始⾏动
• RUNNABLE:可⼯作的.⼜可以分成正在⼯作中和即将开始⼯作.
• BLOCKED:这⼏个都表⽰排队等着其他事情
• WAITING:这⼏个都表⽰排队等着其他事情
• TIMED_WAITING:这⼏个都表⽰排队等着其他事情
• TERMINATED:⼯作完成了.
如果用代码来表示:
NEW状态是,我们创建出了一个线程,但是我们并没有调用start方法启动
RUNNABLE状态是,我们调用了线程的start方法
BLOCKED状态是线程进入阻塞,这个涉及要加锁,后面细说。
WAITING状态时线程等待,也就是调用了上述的join方法和wait方法注意是调用的无参数,也就是死等的方法。
TIMED_WAITING状态也是线程等待,但是是有超时时间的等.
TERMINATED状态时线程执行完的状态
线程状态之间是如何变化的
我们用一张图来解释
线程状态总结:
• BLOCKED表⽰等待获取锁,WAITING和TIMED_WAITING表⽰等待其他线程发来通知.
• TIMED_WAITING表示线程在等待唤醒,但设置了时限;WAITING线程在⽆限等待唤醒
我们可以使用jconsole来查看当前线程的状态,但是有时线程的状态变化很快,我们可以适当的加一些方法让线程进行等待。
(五)多线程带来的风险-----线程不安全
观察线程不安全是什么样子
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
count++;
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
这里如果正常计算应该是1w,但明显结果跟我们预期的不对,这就是发生了线程安全问题。
为什么会出现线程不安全
最根本原因是,线程的调度是随机的,在cpu上是抢占式执行的
其他原因包括:多个线程修改一个变量;线程的操作不是原子的
原子性
这里我们要说一下操作不是原子是什么意思,操作原子并不代表一条指令,上述代码的count++是一条指令,但仍然会发生线程不安全,所以并非是看上去是原子的就行count++实际上是分为三步的:
1.先从内存中拿到count
2.count+1
3.将新count值写回到内存
那如果我们不保证原子性会发生什么呢?
还是以count++为例子,如果有两个线程同时执行count++。第一个线程count已经+1了但是没有写回到内存中。但是第二个线程刚刚从内存中拿到count值,这时候就会发生线程安全问题
这点和线程随即调度,抢占式执行关系很大,如果线程不是抢占式执行,那么就不会发生这种问题。
可见性
可见性是指,一个线程对共享数据的修改,能立刻被其他线程所看到
JMM中规定了
• 线程之间的共享变量存在主内存(MainMemory).
• 每⼀个线程都有⾃⼰的‘⼯作内存"(WorkingMemory)
• 当线程要读取⼀个共享变量的时候,会先把变量从主内存拷⻉到⼯作内存,再从⼯作内存读取数据.
• 当线程要修改⼀个共享变量的时候,也会先修改⼯作内存中的副本,再同步回主内存. 由于每个线程有⾃⼰的⼯作内存,这些⼯作内存中的内容相当于同⼀个共享变量的"副本".此时修改线程1的⼯作内存中的值,线程2的⼯作内存不⼀定会及时变化.
上述我们把实际的内存叫做“主内存”。把cpu缓存,寄存器叫做工作内存方便理解
这里解释一下为什么要有工作内存,为什么不直接从主存中取:
因为cpu缓存和寄存器对数据的处理比较快,而之所以有了缓存和寄存器还需要主存是因为,如果想把缓存和寄存器做的存储量和主存一样大,需要很多“米”。
指令重排序
这里我们可以用单例模式(懒汉方式实现的代码举例)简单说一下,下一篇会讲到多线程实现单例模式,在用懒汉方式下实现的单例模式,我们会在一个方法里真正的创建这个对象,但是在new的时候可能会发生指令重排序,所以我们需要使用volital关键字来解决。
如何解决线程不安全问题(简略)
对上述代码进行改进(使用synchronized加锁)
public static void main(String[] args) throws InterruptedException {
Object lock=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
synchronized (lock){
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
synchronized (lock){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
这样我们的结果就正确了,而synchronized方法我们会在下一篇博客详细讲解