前言:
本文只是对线程有一个基本的认识,多线程编程是Java里非常厚重的一层,如果想深入了解这个知识,必须花费大量的时间精力来学习。
多线程概念:
进程和线程的区别:
进程:每个进程都有独立的代码和数据空间,进程间切换会有较大的开销,一个进程包含1-n个线程(资源分配的最小单位)。
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小(线程是cpu调度的最小单位)。
线程和进程都分五个阶段:创建、就绪、运行、阻塞、终止
多线程是指操作系统能同时运行多个程序。
并发和并行:
并发:同一时刻,多个任务交替执行,就像你开车打电话,大脑是在开车和打电话来回切换的,简单地说,单核Cpu实现的多任务就是并发。
并行:同一个时刻,多个任务同时执行,多核Cpu可以实现并行,就像你开车带着你女朋友,你开车,你女朋友打电话,即为并行。
实现多线程的方法手段:
若想实现多线程分两种方法分别是继承Thread类,或者实现Runable接口。(建议选择实现Runable接口,主要因为Java单继承的特点。)
继承Thread类
public class ThreadTest extends Thread{
private String name;
public ThreadTest(String name){
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(name + "运行 :" + i);
try {
sleep(1000);//休眠1s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadTest a = new ThreadTest("A");
ThreadTest b = new ThreadTest("B");
a.start();
b.start();
}
}
实现Runnable接口
public class ThreadTest2 implements Runnable{
private String name;
public ThreadTest2(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(name + "运行 :" +i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(new ThreadTest2("C")).start();
new Thread(new ThreadTest2("D")).start();
}
}
继承Thread和实现Runnable的区别
如果一个类继承了Thread类,由于Java单继承机制,则不适合资源共享。实现了Runnable接口的话更加容易实现资源共享。
实现Runnable接口比继承Thread类的优势
避免单继承的限制
增加代码的健壮性,代码可以被多个线程共享
线程池只能放入实现Runnable或callable类线程,不能直接放入继承Thread的类
Java运行时线程情况
实际上,Java的main方法也是一个线程,Java中所有的线程都是同时启动的,哪个先执行取决于谁先得到cpu的资源,上图代码中的sleep函数是为了使线程休眠,让其他线程可以获取cpu的资源,每次程序启动时候,main线程就被启动,还有一个垃圾收集线程。但是main线程结束也不影响其他(非守护)线程,但如果其他的是守护线程(使用setDaemon(true)),则main线程退出后,其他线程也退出。
线程状态转换图:
这个图特别重要,首先从左上角来看,新New了一个线程对象,该线程进入初始状态。
调用start函数后,进入了可运行态(这个状态只是表明这个线程可以运行,但仍需要去抢占cpu资源)。
抢到了cpu资源后进入运行状态,在这个状态如果调用sleep函数会让线程进入阻塞态。
在阻塞态的线程是由于某种原因放弃了cpu占用权,暂时停止运行,直到进入就绪态才有机会再次进入运行态,阻塞分为三种,
调用了sleep或join方法,只有当sleep状态超时,或join等待线程终止或者超时线程重新转入就绪态。Sleep不会释放锁
运行的线程执行wait方法,JVM会把该线程放入等待池中,wait会释放锁。
运行的线程获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会使该线程进入上锁态。
死亡状态:线程执行完或者因异常退出run方法,该线程结束生命周期。
线程休眠总结:
sleep、yield、join都会使当前线程让出cpu
sleep会休眠一段时间,不考虑其他线程的优先级,放弃cpu之后转为阻塞态。
yield没有时间间隔,它会使线程重新回到就绪态来争夺cpu
jon是会立即放弃cpu不考虑其他线程的优先级,放弃cpu后转为阻塞态。
线程同步
线程是一个独立运行的程序,有自己的运行栈,线程可能和其他线程共享文件、内存资源等
多线程同时读写一部分资源,会出现冲突,就像实现线程示例代码展示的,他们是混乱执行,线程同步就是排队,就像你上公共厕所要排队,一个个来对共享资源操作。
实现线程同步:
确保一个时间点只有一个线程访问共享资源,就像在外面上公共厕所,每一个时刻只有一个人能够进入方便,公共厕所是给门加了一把锁,而在java里也是这样做的,可以给共享资源加一把锁即Synchronized关键字同步方法或者代码块。
同步方法:
同步方法是使用synchronized修饰的方法格式为访问修饰符 synchronized 返回类型 方法名(){}
public synchronized void thread1(){
}
锁定的是调用这个方法的对象,其他线程不能访问这个对象的任何一个synchronized方法
不同的对象实例的synchronized方法是互相不干扰的,其他线程同样可以访问相同类的另一个对象中的synchronized方法
使用同步方法可以方便的将类变成线程安全的类。
同步代码块:
如果没有明确的对象上锁,只想让一块代码实现线程同步时,可以用同步代码块,
private byte[] lock = new byte[0];
synchronized(lock){
//被同步的代码
}
线程通信
实现线程通信:
Java代码基于对共享数据进行wait()、 notify() 、notifyAll()来实现多个线程的通讯
wait():
导致当前线程等待,知道其他线程调用此对象的notify()方法或notifyAll()方法。
notify():
唤醒在此对象监视器上等待的单个线程。
notifyAll():
唤醒再次对象监视器上等待的所有线程。
线程死锁的概念:
线程死锁指的是两个线程互相持有对方的共享资源,拿老韩举的一个很形象的例子,小明妈妈和小明,小明说我先看电视再写作业,小明妈妈说你先写作业再看电视,造成了无限阻塞。
导致死锁的根源在于“synchronized”关键词的不恰当使用
解决死锁的办法:
让线程持有独立的资源
尽量不采用嵌套的synchronized语句。