一、基本概念
进程和线程的关系
进程可以理解为应用程序,一个程序同时执行多个任务。通常,每一个任务称为一个线程(thread
)。比如一个团队如果是一个“进程”,那么团队中的每个人都各司其职,就是多个“线程”。
二者的本质区别在于进程之间内存独立,互不影响;一个进程下的线程之间共享数据,即共享堆内存和方法区,但是栈内存是相互独立的。
注释
单核CPU
不能做到真正意义上的多线程并发,即一个时间点上只能处理一件事情。但是CPU处理速度极快,可以在多个线程之间频繁切换执行,给人的感觉是多线程并发。
二、创建新的执行线程的方法
1、将一个类声明为Thread
的子类。 这个子类应该重写Thread
类的run()
方法,示例如下:
public class ThreadTest {
public static void main(String[] args){
CountThread countThread=new CountThread();
//启动分支进程
countThread.start();
//主线程的代码
for(int i=1;i<100;i++)
System.out.println("主线程---"+i);
}
}
//数数进程
class CountThread extends Thread{
@Override
public void run() {
for(int i=1;i<100;i++)
System.out.println("分支线程---"+i);
}
}
结果
主线程和分支线程同时运行,控制台计数无规律,下面是截取的一段输出:
分支进程---10
主进程---39
分支进程---11
注释
start()
方法启动一个分支线程,在JVM
中开辟一个新的栈空间,完成任务之后,瞬间就结束了,开始运行下一行代码;- 如果没有
start()
,而是单纯地调用countThread
的run()
方法,就只是普通的方法调用,分支线程并没有被启动; run()
中的异常只能try...catch...
捕获,不能throws
抛出。因为run()
在父类中没有抛出异常,子类就不能抛出更多的异常。
2、创建一个实现类Runnable
接口的类,并且实现了run()
方法。 然后在创建Thread
时作为参数传递,并启动。示例如下:
public class ThreadTest {
public static void main(String[] args){
//创建分支进程
Thread aThread=new Thread(new CountThread());
//启动分支进程
aThread.start();
//主线程的代码
for(int i=1;i<100;i++)
System.out.println("主进程---"+i);
}
}
//数数进程
class CountThread implements Runnable{
@Override
public void run() {
for(int i=1;i<100;i++)
System.out.println("分支进程---"+i);
}
}
注释
由于Java是单继承,一旦用第一种方法创建进程,以后再继承其他类就不方便,所以第二种方法相对来说灵活一些。
3、创建一个实现Callable<V>
接口的类,好处在于可以获取接口中call
方法的返回值,但是后续的代码必须要等待call方法执行完毕之后才能进行。
三、线程的状态
四、线程常用方法
方法 | 解释 |
---|---|
static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
String getName() | 返回此线程的名称 |
void setName(String name) | 将此线程的名称更改为等于参数 name |
static void sleep(long millis) | 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性 |
void interrupt() | 中断这个线程,即让正在sleep的进程提前解除阻塞 |
注释
currentThread()
的返回值与Thread.currentThread
调用的位置有关,如果在main()
所在的类中调用,返回的是主线程;如果在继承Thread
的类中调用,返回的就是该类所对应的分支线程。- 线程对象创建时的默认名称是
Thread-0、Thread-1...
; - 主线程的默认名字是
main
; sleep()
作用是让当前进程进入阻塞状态,放弃占有的CPU时间片。写有Thread.sleep(long millis)
的线程进行休眠,通常用来间隔时间执行代码。interrupt()
运用了Java的异常处理机制,调用后会让sleep()
的try...catch...
捕获异常,然后接着执行run()
中的代码,达到解除阻塞的目的。
示例
1、对sleep方法的理解
public class ThreadTest {
public static void main(String[] args){
//创建分支进程
Thread countThread=new CountThread();
//启动分支进程
countThread.start();
//分支进程会阻塞2s吗?
try {
countThread.sleep(2*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//主线程的代码
for(int i=1;i<100;i++)
System.out.println("主进程---"+i);
}
}
//数数进程
class CountThread extends Thread{
@Override
public void run() {
for(int i=1;i<100;i++)
System.out.println("分支进程---"+i);
}
}
countThread.sleep(2*1000)
会自动转化为Thread.sleep(2*1000)
,因为sleep()
是静态方法,最终还是主线程会进入阻塞态2秒。想要countThread
阻塞,就必须在CountThread
类中写入sleep()
。
2、终止线程的合理方式
public class ThreadTest {
public static void main(String[] args){
//创建分支进程
CountThread countThread=new CountThread();
//启动分支进程
countThread.start();
//主线程的代码
//想让分支线程在2秒后终止
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//改变标志位
countThread.flag=false;
}
}
//数数进程
class CountThread extends Thread{
//设置标志位
Boolean flag=true;
@Override
public void run() {
for(int i=1;i<5;i++){
if(flag){
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{
//到这里线程终止
//可以写一些终止之后的代码放在这里,以免数据丢失
}
}
}
}
强行终止线程的方法如countThread.stop()
,有一个致命的缺点就是容易导致数据丢失,不建议使用!
五、线程调度
线程调度是指按照特定机制为多个线程分配CPU的使用权。Java中采用抢占式调度模型,每当线程调度器有机会选择新线程时,首先选择具有较高优先级的线程,即这些线程抢到CPU时间片的概率就高一些。
常用方法
方法 | 解释 |
---|---|
int getPriority() | 返回此线程的优先级 |
void setPriority(int newPriority) | 更改此线程的优先级 |
static void yield() | 对调度程序的一个暗示,即让当前线程重新进入就绪态 |
void join() | 合并线程 |
注释
- 优先级范围
[1,10]
,默认优先级是5。优先级高的线程抢到CPU时间片的概率更高,程序运行时大概率偏向优先级高的线程。 yield()
不是让线程进入阻塞态,而是从运行态回到就绪态,进而重新抢夺CPU时间片。同样地,Thread.yield()
要写到对应的线程中才会起作用。join()
方法让当前线程进入阻塞状态,直到调用join()
的线程结束为止,比如在main()
方法中如果有其它线程调用这个方法,主线程就会受阻。
六、线程安全
1、线程同步机制
数据在多线程并发的环境下会存在安全问题,因为共享数据的缘故,当多个线程对数据做出修改时,数据的值变化量就无法控制了。例如:
//多个老师对学生试卷做出评判,在100分的基础上往下扣
//数据类
public class Paper{
private String name;
private int scores;
public Paper(String name,int scores){
this.name=name;
this.scores=scores;
}
//得到姓名
public String getName() {
return name;
}
//得到分数
public int getScores() {
return scores;
}
//设置分数
public void setScores(int scores) {
this.scores = scores;
}
//修改试卷
public void modify(int deduction){
int begin=getScores();
int end=begin-deduction;
//模拟网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setScores(end);
}
}
//分支线程类
public class PaperThread extends Thread{
private Paper paper;
public PaperThread(Paper paper){
this.paper=paper;
}
@Override
public void run() {
paper.modify(10);
System.out.println(Thread.currentThread().getName()+"扣了"+paper.getName()+"10分,最终得分"+paper.getScores());
}
}
//测试类
public class TestPaper {
public static void main(String[] args){
Paper aPaper=new Paper("小明",100);
PaperThread thread0=new PaperThread(aPaper);
PaperThread thread1=new PaperThread(aPaper);
thread0.start();
thread1.start();
}
}
测试过程中学生的最终分数随着测试次数的增加不尽相同,可能会出现一个老师扣了10分,最终学生却得到了80分。如果用sleep模拟网络延迟,修改分数一定跟预期不符。
解决线程安全问题可采用“线程同步机制”—取消线程的并发执行,即必须排队修改数据。对应同步编程模型,即一个线程执行的时候,必须要等待另一个线程结束,两个线程间产生了等待的关系。虽然降低了效率,但是保证了数据的安全。
解决办法
//修改试卷
public void modify(int deduction){
synchronized (this) {
int begin = getScores();
int end = begin - deduction;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setScores(end);
}
}
注释
synchronized ()
括号里的参数一定是需要排队执行的线程所共享的数据,这里用this就表明测试用例中需要完成同步的是paper对象;- 执行原理:在Java中,每个对象都有一把锁,执行到
synchronized (this){ }
中同步代码块的程序,先执行的线程拿到对象锁,此时其它线程无法进入,直到对象锁被释放; public synchronized void modify(int deduction)
也是可以的,只不过默认锁this对象,同步整个方法体,有可能降低效率,但代码简洁。静态方法是类锁,所有的类对象共同拥有这把锁;- 局部变量不会有线程安全问题,因为它存在于每个线程的栈中。相反地,存在于方法区中的静态变量和存在于堆中的实例变量则存在线程安全问题。
2、死锁
一般嵌套写synchronized
会产生这样一个现象:两个线程都需要对方释放各自需要的对象锁才能继续运行,但是双方互不让步,导致程序瘫痪。
public class DeadLock {
public static void main(String[] args){
Object obj1=new Object();
Object obj2=new Object();
Thread thread1=new TestThread1(obj1,obj2);
Thread thread2=new TestThread2(obj1,obj2);
thread1.start();
thread2.start();
}
}
class TestThread1 extends Thread {
private Object obj1;
private Object obj2;
public TestThread1(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
//拥有obj1的对象锁
synchronized (obj1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//需要得到obj2的对象锁
synchronized (obj2) {
}
}
}
}
class TestThread2 extends Thread {
private Object obj1;
private Object obj2;
public TestThread2(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
//拥有obj2的对象锁
synchronized (obj2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//需要得到obj2的对象锁
synchronized (obj1) {
}
}
}
}
3、守护线程
Java中的线程分为用户线程和守护线程(后台线程)。一般守护线程是一个死循环,只要所有的用户线程结束,守护线程就会自动结束,如垃圾回收线程。Thread
类中的void setDaemon(boolean on)
用来设置线程的类型。
4、定时器
实际应用中,可能需要定时执行一些任务,如数据的备份,所以就需要用到定时器。示例如下:
//功能:每隔3秒打印当前时间
public class TestTimer {
public static void main(String[] args) throws ParseException {
//设置开始日期
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date begintime=sdf.parse("2020-04-23 22:12:00");
//设置定时器
Timer clockTimer=new Timer();
clockTimer.schedule(new Clock(),begintime,1000*3);
}
}
//需要完成的定时任务
class Clock extends TimerTask{
@Override
public void run() {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String nowtime=sdf.format(new Date());
System.out.println("当前时间:"+nowtime);
}
}
5、生产者消费者模式
生产者消费者模式是多线程编程中非常重要的设计模式,生产者负责生产数据,消费者负责消费数据。需要用到Object类的两个方法:
void wait()
:让当前占有使用该方法的对象的进程进入等待状态,并且释放线程占有的对象锁;void notify()
:唤醒在当前对象上活动且进入等待状态的线程,但并不会释放当前线程占有的对象锁。
public class TestMode {
public static void main(String[] args){
List warehouse=new ArrayList();
Thread produceThread=new Thread(new Produce(warehouse));
Thread conductThread=new Thread(new Conduct(warehouse));
produceThread.start();
conductThread.start();
}
}
//生产者
class Produce implements Runnable{
private List warehouse;
public Produce(List warehouse){
this.warehouse=warehouse;
}
@Override
public void run() {
while(true){
synchronized (warehouse){
//仓库满了之后唤醒消费者
if(warehouse.size()>3){
try {
warehouse.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//抢到锁而且仓库不满时生产,并唤醒消费者
else{
Object obj=new Object();
warehouse.add(obj);
System.out.println("生产一个,仓库存货:"+warehouse.size());
warehouse.notify();
}
}
}
}
}
//消费者
class Conduct implements Runnable{
private List warehouse;
public Conduct(List warehouse){
this.warehouse=warehouse;
}
@Override
public void run() {
while(true){
synchronized (warehouse){
//消费完了之后唤醒生产者
if(warehouse.size()==0){
try {
warehouse.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//抢到锁并且有库存时消费,并唤醒生产者
else{
warehouse.remove(warehouse.size()-1);
System.out.println("消费一个,仓库存货:"+warehouse.size());
warehouse.notify();
}
}
}
}
}
码字不易,还请多多支持,可评论交流~👍