目录
2.2.1 编写一个类,实现java.lang.Runnable 接口,实现run方法
4.7 线程优先级 setPriority () & getPriority()
前言
本文记录JAVA线程相关的学习笔记,课程来源为动力节点,文章内容参考了站内相关技术文章,如有冒犯,请联系我。qwq
一、进程与线程
1.1 区别
进程(Process)是一个应用程序(一个软件)
线程(Thread)是一个进程中的执行场景/执行单元,可以理解为进程中执行的一段程序片段,是进程的一个执行单元,是进程内可调度实体,是比进程更小的独立运行的基本单位,线程也被称为轻量级进程
一个进程可以启动多个线程
1.2 关系
一个公司是一个进程
公司内的一个员工是一个线程
1.3 内存共享问题
进程 A 和进程 B 的内存独立不共享。(两个公司的资源不会共享)
线程 A 和线程 B
java中 线程A和线程B 堆内存和方法区内存共享
但是栈内存独立,一个线程一个栈
启动多个线程就有多个栈,各自互不干扰:多线程并发
火车站是一个进程;每一个售票窗口是一个线程;在每个窗口都可以买票,多线程并发可以提高效率
java中多线程机制目的就是为了提高程序的处理效率
main方法结束只是主线程结束,主栈空了,其他的栈(线程)可能还在压栈弹栈
1.4 启动一个HelloWord会发生什么
对于Java程序说,HelloWorld 的执行,会先启动JVM,而JVM就是一个进程
JVM再启动一个主线程调用main方法
同时再启动一个垃圾回收线程负责看护(守护线程),回收垃圾
现在的JAVA程序中至少有两个线程并发,一个是垃圾回收进程一个是执行main方法的主线程
二、创建线程
2.1 继承Thread类实现多线程
编写一个类,直接继承 java.lang.Thread, 重写run方法
//创建线程类,继承 Thread class MyThread extends Thread{ private String name; public MyThread(String name) { this.name = name; } //重写 run() @Override public void run() { for(int i = 0 ; i < 100 ; i++) { System.out.println(this.name + "正在工作中……" + i); } } } public class MyThreadText{ //这里是main方法,这里的代码属于主线程,在主栈中运行 public static void main(String[] args) { //实例化分支线程对象 MyThread myThread1 = new MyThread("线程1"); MyThread myThread2 = new MyThread("线程2"); MyThread myThread3 = new MyThread("线程3"); //启动线程 myThread1.start(); myThread2.start(); myThread3.start(); } }
2.2 实现Runnable接口实现多线程
一个类实现了接口,它还可以去继承其它的类,比继承Thread实现多线程更灵活
2.2.1 编写一个类,实现java.lang.Runnable 接口,实现run方法
//创建一个类,实现 Runnable 接口 class Myrunnable implements Runnable{ private String name; public MyThread(String name) { this.name = name; } //实现 run() 方法 @Override public void run() { for(int i = 0 ; i<50 ;i++) { System.out.println(this.name + " 正在执行中……" + i); } } } public class MyThreadTest{ public static void main(String[] args){ //实例化 Thread t = new Thread(Myrunnable()); //启动线程 t.start(); for(int i =0;i<100;i++){ System.out.println("主线程--->"+i); } } }
2.2.2 采用匿名内部类的方式创建线程对象
public class MyThreadTest{ public static void main(String[] args){ //实例化线程对象 Thread t = new Thread(Myrunnable(){ @Override public void run(){ for(int i =0;i<100;i++){ System.out.println("子主线程--->"+i); } } }); //启动线程 t.start(); for(int i =0;i<100;i++){ System.out.println("主线程--->"+i); } } }
2.3 实现Callable接口实现多线程
继承Thread类和实现Runnable接口,其重写的run()方法都没有返回值,而实现Callable接口,可以解决这个问题。
Callable接口实现采用泛型技术实现,继承需要重写call方法,再通过FutureTask包装器包装,传入后实例化Thread类实现多线程。
其中FutureTask类是Runnable接口的子类,所以才可以利用Thread类的start方法启动多线程
//创建类实现Callable接口
public class MyThread implements Callable<Integer>{
private String name;
public MyThread(String name) {
this.name = name;
}
//重写call()方法 可以看作是有返回值的run()方法
@Override
public Integer call(){
Integer sum = 0;
for(int i = 0 ; i < 500;i++) {
System.out.println(this.name + i);
sum += i;
}
return sum;
}
}
import java.util.concurrent.FutureTask;
public class testThread {
public static void main(String[] args) throws Exception{
// 实例化继承Callable接口的MyThread类
MyThread mt1 = new MyThread("线程一");
MyThread mt2 = new MyThread("线程二");
MyThread mt3 = new MyThread("线程三");
// FutureTask类接收继承Callable接口的MyThread的实例
FutureTask<Integer> ft1 = new FutureTask<Integer>(mt1);
FutureTask<Integer> ft2 = new FutureTask<Integer>(mt2);
FutureTask<Integer> ft3 = new FutureTask<Integer>(mt3);
// 启动多线程
new Thread(ft1).start();
new Thread(ft2).start();
new Thread(ft3).start();
System.out.println(ft1.get());
System.out.println(ft2.get());
System.out.println(ft3.get());
}
}
2.4 线程池
2.4.1 线程池概念
容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程的操作,无需反复创建线程而消耗过多的资源
2.4.2 什么要使用线程池
如果并发的线程数量过多,并且每个线程都是执行一个时间很短的任务就结束,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要消耗时间,线程也属于宝贵的系统资源,因此,线程池就是为了能使线程可以复用而创建的。
- 降低资源的消耗,减少创建和销毁线程的次数,每个工作线程都可以被重复使用,可执行多个任务
- 提高响应速度,不需要频繁地创建线程,如果有线程可以直接使用,避免了系统僵死
- 提高线程的可管理性
2.4.3 线程池工作原理
2.4.4 简单实现
1)Runnable
public class Test {
public static void main(String[] args) {
// 1.创建一个线程池,指定线程的数量为2
ExecutorService pools = Executors.newFixedThreadPool(2);
// 2.添加线程任务
Runnable myThread= new MyRunnable();
pools.submit(myThread); // 第一次提交任务,此时创建新线程
pools.submit(myThread); // 第二次提交任务,此时创建新线程
pools.submit(myThread); // 第三次提交任务,复用之前的线程
pools.shutdown(); // 当所有任务全部完成后才关闭线程池
// pools.shutdownNow(); // 立即关闭线程池
}
}
//实现Runnable的线程类
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0 ; i<10 ; i++) {
System.out.println(Thread.currentThread().getName()+"正在执行任务… "+i);
}
}
}
三、线程生命周期
任意线程具有5种基本状态:
新建状态:创建一个线程对象后,该线程对象处于创建状态,此时它已经有了内存空间和其它资源,但它还是处于不可运行的
就绪状态:新建线程对象后,调用该线程的start方法启动该线程,启动后,进入线程队列排队,由CPU调度服务
运行状态:就绪状态的线程获得处理器的资源时,线程就进入了运行状态,此时将自动调用run方法
阻塞状态:正在运行的线程在某些特殊情况下,如:当前线程调用sleep、suspend、wait等方法时,运行在当前线程里的其它线程调用join方法时,以及等待用户输入的时候,停止运行,当引起阻塞原因消失后,线程才能进入就绪状态
死亡状态:当线程run方法运行结束后、线程抛出未捕获的Exception或者Error、调用该线程是stop()方法,线程才能处于终止状态,线程一旦死亡就不能复生
注意:当主线程结束时,其他线程(严格来说是非守护线程)不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受到主线程结束的影响
四、线程操作的常用方法
- 线程的命名与获取:
- 判断线程是否启动: isAlive()
- 线程休眠方法:sleep()
- 线程中断方法:interrupt()
- 线程强制执行:join()
- 线程让步:yield()
- 线程优先级:setPriority () getPriority()
- 设置守护线程:setDaemon()
4.1 线程的命名与获取
在线程操作中,如果没有为一个线程指定一个名称,则系统在使用会自动为线程分配一个名称,格式为 Thread-xx
- public Thread(Runnable runnable,String name) //构造函数:实例化线程对象,为线程对象设置名称
- public final void setName(String name) // 普通函数:设置线程名字
- public final String getName() // 普通函数:获取线程名字
class MyThread implements Runnable{
public void run(){
System.out.println("当前执行的线程名称是" + Thread.currentThread().getName());
}
}
public class ThreadNameDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt,"线程-A").start();
new Thread(mt,"线程-B").start();
new Thread(mt).start();
new Thread(mt,"线程-C").start();
new Thread(mt).start();
}
}
4.2 判断线程是否启动 isAlive()
class MyThread implements Runnable{
public void run(){
System.out.println("当前执行的线程名称是:" + Thread.currentThread().getName());
}
}
public class ThreadNameDemo02 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t = new Thread(mt,"线程");
System.out.println("线程开始执行之前--->" + t.isAlive()); //判断是否启动
t.start(); //启动线程
System.out.println("线程开始执行之后--->" + t.isAlive()); //判断是否启动
for (int i=0; i<3; i++) {
System.out.println("main方法运行" + i);
}
//以下输出结果不确定
System.out.println("代码执行之后--->" + t.isAlive()); //输出结果不确定
}
}
由于线程操作的不确定性,如果线程率先执行完成,则最后结果为false,如果主线程先执行完成,那么其它线程也不会收到影响,并不会随着主线程的结束而结束,所以最后为 true
4.3 线程休眠方法 sleep()
sleep方法定义在java.lang.Thread中,由Thread.sleep()调用实现。其作用是需要暂缓线程的执行速度,则可以让当前线程休眠,即当前线程从“运行状态”进入到“阻塞状态”。sleep方法会指定休眠时间,线程休眠的时间大于或等于指定的休眠时间,该线程会被唤醒,此时它会由“阻塞状态”变成“就绪状态”,然后等待CPU的调度执行
class MyThread implements Runnable{
public void run(){
for (int i=0; i<5; i++){
try {
Thread.sleep(500); //线程睡眠500毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行--->" + i);
}
}
}
public class ThreadJoinDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t = new Thread(mt,"线程");
t.start();
}
}
4.4 线程中断 interrupt()
public boolean isInterrupted() //普通函数:判断线程是否被中断
public void interrupt() //普通函数:中断线程执行
该方法将会设置该线程的中断状态位,即设置为true,中断的结果线程是终止状态、还是阻塞状态或是继续运行至下一步,就取决于该程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(即中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程
public class MyThreadText{
public static void main(String[] args) throws Exception {
Thread thread = new Thread(()-> {
System.out.print("线程休眠1000毫秒\n");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("休眠中断!");
}
});
thread.start();
Thread.sleep(100);
if(!Thread.interrupted()) {
System.out.println("中断thread线程的休眠!");
thread.interrupt();
}
}
}
4.5 线程强制执行 join()
join方法定义在java.lang.Thread中,由Thread.join()调用实现。
多线程启动后会交替进行资源抢占和线程体执行,如果此时某些线程异常重要,也就是说这个对象需要优先执行完成,则可以设置为线程强制执行,待其完成后其它线程继续执行
public class MyThread{
public static void main(String[] args) throws Exception {
Thread mainthread=Thread.currentThread();//获得主线程
Thread thread = new Thread(()-> {
for(int i=0;i<50;i++) {
if(i>3) {
try {
//主线程强制执行
mainthread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("子线程在执行!"+i);
}
});
//启动子线程
thread.start();
//主线程程序
for(int i=0;i<50;i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("主线程在执行!"+i);
}
}
}
4.6 线程让步 yield()
public static void yield() // 静态函数:线程让步
yield方法定义在java.lang.Thread中,由Thread.yield()调用实现。
多线程在彼此交替执行的时候往往需要进行资源的轮流抢占,如果某些不是很重要的线程抢占到资源但是又不急于执行时,就可以将当前的资源暂时让步出去,交给其它资源先执行。但是,因为yeild是将线程由“运行状态”转别为“就绪状态”,这样并不能保证在当前线程调用yield方法之后,其它具有相同优先级的线程就一定能获得执行权,也有可能是当前线程又进入到“运行状态”继续运行,因为还是要依靠CPU调度才可以
- 线程直接由运行状态跳回预备状态。
- 让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态
- 调用了yield方法之后,如果没有其他等待执行的线程,此时当前线程就会马上恢复执行!
public class MyThreadText {
public static void main(String[] args) throws Exception {
MyThread myThread = new MyThread();
new Thread(myThread,"t1").start();
new Thread(myThread,"t2").start();
new Thread(myThread,"t3").start();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "正在工作"+i);
System.out.println(Thread.currentThread().getName() + "让步");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "正在工作结束");
}
}
}
运行结果:礼让后不一定按照顺序执行,礼让只是让其返回就绪状态,重新等待CPU调度
4.7 线程优先级 setPriority () & getPriority()
- 线程的优先级代表的是概率
- 范围从1到10,默认为5
- 优先级相对最高的线程在执行时被给予优先权限,但是不能保证线程在启动时就进入运行状态,优先级越高越有可能先执行。
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪个线程来执行。
- 优先级高的可能执行的时间片多一些,执行的早一点,不代表先后顺序。
public static final int MAX_PRIORITY // 静态常量:最高优先级,数值为10
public static final int NORM_PRIORITY //静态常量:普通优先级,数值为5
public static final int MIN_PRIORITY // 静态常量:最低优先级,数值为1
public final void setPriority(int newPriority) // 普通函数:设置优先级
public final int getPriority() //普通函数:获取优先级
public class PriorityTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getPriority());
MyPriority mp = new MyPriority();
Thread t1 = new Thread(mp);
Thread t2 = new Thread(mp);
Thread t3 = new Thread(mp);
Thread t4 = new Thread(mp);
Thread t5 = new Thread(mp);
Thread t6 = new Thread(mp);
//设置优先级要在启动之前
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(2);
t3.setPriority(4);
t4.setPriority(6);
t5.setPriority(8);
t6.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}
class MyPriority implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
}
4.8 设为守护线程 setDaemon()
线程分为用户线程和守护线程。
虚拟机必须确保用户线程执行完毕。虚拟机不用等待守护线程执行完毕,如后台记录操作日志、监控内存使用等。
- 可以将指定的线程设置成后台线程,守护线程
- 创建用户线程的线程结束时,后台线程也随之消亡
- 只能在线程启动之前把它设为后台线程
public class DaemonTest {
public static void main(String[] args){
MyDaeomon myDaeomon = new MyDaeomon ();
MyThread myThread = new MyThread ();
Thread t = new Thread(myDaeomon);
t.setDaemon(true); //将用户线程调整为守护
t.start();
new Thread(myThread).start();
}
}
class MyThread implements Runnable{
public void run(){
for(int i=1;i<=365*100;i++){
System.out.println("happy life...");
}
System.out.println("over!");
}
}
class MyDaeomon implements Runnable{
public void run(){
while(true){
System.out.println("bless you...");
}
}
}
五、线程的同步和锁死
5.1 线程安全
什么时候数据在多线程并发的环境下会存在安全问题?
条件1、多线程并发
条件2、有共享数据
条件3、共享数据有修改的行为(对资源进行非原子性操作)
满足以上三个条件,就会存在线程安全问题
举例:正常逻辑下,同一个编号的火车票只能售出一次,却由于线程安全问题而被多次售出,从而引起实际业务异常
5.2 解决线程安全问题
解决数据共享问题必须使用同步,所谓的同步就是指多个线程在同一个时间段内只能有一个线程执行指定的代码,其他线程要等待此线程完成之后才可以继续进行执行,在Java中提供有
synchronized
关键字以实现同步处理,同步的关键是要为代码加上“锁”。有三种操作:
1.同步代码块
2.同步方法
3.Lock实现
5.3 同步代码块
synchronized(需要同步的对象){
需要同步的操作
}
注意:
- 同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。所以,锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,线程之间便不能产生同步的效果。
- 比如在实现Runnable接口的时候,我们可以用this来作为锁,this就是当前对象,因为线程类共用了一个对象,所以可以使用this作为锁
- 但是我们在使用继承Thread的方式的时候,因为我们创建了不同的对象,所以再使用this就不是同一把锁了,就还会导致多线程安全问题。所以我们一般使用线程类的
.class
作为锁
public class MyThread implements Runnable{
private int ticket = 10;
@Override
public void run() {
while(true) {
// 同步代码块
synchronized(this) {
if(ticket<0) {
System.out.println(Thread.currentThread().getName() + "的票已经全部售完,此时的票数量为:"+ticket);
break;
}
try {
Thread.sleep(10); // 延迟0.01秒,使得ticket可以被其它线程充分改变(可能此时的ticket小于等于0了)
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在售票,还剩余票数为:" + ticket--);
}
}
}
}
//测试
public class ThreadDemo {
public static void main(String[] args) {
// 一份资源
Runnable mt1 = new MyThread();
// 共享同一份资源
new Thread(mt1,"售票员A").start();
new Thread(mt1,"售票员B").start();
new Thread(mt1,"售票员C").start();
}
}
运行情况:
不加锁运行:
5.4 同步方法
使用方式
synchronized 返回值类型 方法名(参数1, ...){ }
public class MyThread implements Runnable{
private int ticket = 10;
@Override
public void run() {
while(this.sale()) {}
}
public synchronized boolean sale() {
if(ticket<0) {
System.out.println(Thread.currentThread().getName() + "的票已经全部售完,此时的票数量为:"+ticket);
return false;
}
try {
Thread.sleep(10); // 延迟0.01秒,使得ticket可以被其它线程充分改变(可能此时的ticket小于等于0了)
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在售票,还剩余票数为:" + ticket--);
return true;
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 一份资源
Runnable mt1 = new MyThread();
// 共享同一份资源
new Thread(mt1,"售票员A").start();
new Thread(mt1,"售票员B").start();
new Thread(mt1,"售票员C").start();
}
}
同步方法也有自己的锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。
这样做的好处是,同步方法被所有线程共享,方法所在的对象相对于所有线程来说也是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止,从而达到了线程同步的效果。但是,有时候我们的方法需要是静态方法,静态方法不需要创建对象就可以直接使用“类名.方法名”的方式调用。这个时候,我们都没有对象,那么同步方法的锁就不会是this,那是什么呢?
Java中静态方法的锁是该类所在类的class对象,该对象可以直接使用类名.class的方式获取。
- 不要将run()定义为同步方法
- 非静态同步方法的同步监视器是this,静态同步方法的同步监视器是类名.class 字节码信息对象
- 同步代码块的效率要高于同步方法,原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
- 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
5.5 Lock锁
package cn.wu;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable{
private int ticket = 10;
//定义lock锁,ReentrantLock是Lock接口的实现类
private final Lock lock = new ReentrantLock();
@Override
public void run() {
while(this.sale()) {}
}
public boolean sale() {
//加锁,在要变化的量加锁
lock.lock();
try{
if(ticket<0) {
System.out.println(Thread.currentThread().getName() + "的票已经全部售完,此时的票数量为:"+ticket);
return false;
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName() + " 正在售票,还剩余票数为:" + ticket--);
}catch (Exception e) {
e.printStackTrace();
}finally {
//解锁 把解锁放在finally里
lock.unlock();
}
return true;
}
}
synchronized和Lock的对比
- Lock是显示锁,需要手动开启和关闭,synchronized是隐式锁,出了作用域(作用域:方法或代码块)自动释放
- Lock只有代码块锁,synchronized有方法锁和代码块锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有较好的扩展性,提供更多子类(如ReentrantLock类)
优先使用顺序:Loc>同步代码块(在方法体内用,已经分配了资源)>同步方法(在方法体外使用)