JAVA多线程基础续集

Thread类中的常见方法

上一次,我们讲解了线程的基本概念,以及构造线程的几种方法,这一次我们来聊一聊,为什么要这么构建,以及构建完成线程以后,我们可以做一些什么事情

构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程
Thread(String name)创建一个线程使用name命名
Thread(Runnable target,String name)根据Runnable对象来创建线程,并用name命名
class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("hello ");
    }
}
public class Demo3 {
    public static void main(String[] args) {
   		 Thread t1 = new Thread();
   		 Thread t2 = new Thread(new MyRunnable);
   		 Thread t3 = new Thread("这是一个线程的名字");
   		 Thread t4 = new Thread(new MyRunnable(),"这是另一个线程的名字");
	}
}

常见属性以及Getter

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同的线程ID不会重复
  • 名称是各种调试工具使用到的
  • 状态标识线程当前所处的情况 何为线程状态
  • 优先级高的线程,理论上来说更容易被调度到
  • JVM在一个进程的所有非后台线程结束后,才会结束运行
    • 如果线程是后台线程,不影响进程的退出
    • 如果线程不是后台线程,就会影响线程退出
  • run方法运行结束后,线程不再存活
    • 操作系统中的线程是否正在运行,Thread对象的生命周期与内核中对应的线程生命周期并不完全一致,创建出线程对象之后,在调用start之前,系统中是没有对应线程的,在run方法执行完成之后,系统中的线程就已经销毁了,但是java对象可能还存在,通过isAlive就能判定当前系统的线程运行情况
    • 如果调用start之后,run执行完之前,isAlive返回true
    • 如果调用start之前,或run执行完之后,isAlive就返回false
  • 线程的中断 何为线程中断

Thread中的一些重要方法

中断线程

线程停下来的关键,就是让线程对应的run方法执行完毕
对于main 来说,main方法执行完了,线程就结束了

操作方法

  1. 可以手动设置一个标志位来控制线程是否要结束
public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
    	public static boolean isQuit = false;
        Thread t = new Thread(()->{
            while (!isQuit){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        t.start();
        Thread.sleep(3000)
        isQuit = true;
    }
}
  1. 可以使用Thread中内置的一个标志位来进行判定
    1. Thread.interrupted();
    2. Thread.currentThread.isInterrupted();实例方法,其中currentThread可以获取到当前线程的实例对象
public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        t.start();
        Thread.sleep(5000);
        t.interrupt();
    }
}

调用t.interrupt()可能出现两种情况

  1. 如果t线程处于休眠状态,就直接设置线程的标志位为true
  2. 如果线程处于阻塞状态(休眠),就会触发一个InterruptException
    1. 调用这个intertupt方法,就会让 sleep触发一个异常,从而导致线程从阻塞状态中被唤醒
    2. 当出现异常之后,进入catch段,使用break;就可以使得线程终止

关于线程中断的一些问题

判断线程是否中断,推荐使用isInterrupted()方法

  1. interrupted()这个方法判定的标志位是Thread 的静态成员,这就导致了,一个程序中只有一个标志位,这显然是不合理的
  2. Thread.currentThread().isInterrupted()这个方法判定的是Thread的普通成员,每个实例都有自己的标志位 —也就是下图这个东西
    在这里插入图片描述

线程等待

线程等待,是控制线程执行顺序的一种手段,此处的线程等待,主要是控制线程结束的先后顺序

线程等待的方法

join

  • 调用join的时候,哪个线程调用的join,哪个线程就会进入阻塞等待,直到join进来的线程执行完毕为止
  • join操作默认情况下,会一直等待到join进来的线程结束为止(没有超时时间)
  • 可以手动的指定超时时间,如果超过指定的时间,线程仍然没有结束,join也会 直接返回
    在下图的代码段中,main线程会等待t线程 10s,如果再10s内,t线程执行完毕,那么main线程就会解除阻塞,继续运行,否则十秒后才会继续运行
    线程等待示例代码

获取线程的引用

Thread.currentThread()就可以获取到当前线程的引用,使用哪个线程调用的currentThread()得到的就是哪个线程的对象

我为什么要是使用这个方法?我不是定义过了一个变量吗

一般都是在没有线程对象又需要获得线程信息时通过Thread.currentThread()获取当前代码段所在线程的引用,比如使用Runnable 或者 Callable接口创建线程的时候

线程休眠

  • sleep() 无休止的睡下去
  • sleep(int time) 睡一会儿

线程的状态

进程是有状态的

  • 就绪
  • 阻塞
    这里的状态决定了系统按照什么样的态度来调度这个进程(相当于是针对 一个进程中只有一个线程的情况)
    更常见的情况下:一个进程包含了多个线程,所谓的状态,其实是绑定在线程上
    就绪 和 阻塞 都是针对系统层面上

在Java Thread类中,对于线程的状态,进一步细化了

TERMINATED: 操作系统中的线程已经执行完毕,销毁了,但是对象还存在

RUNNABLE:处于就绪状态的线程,随时可以被调度到CPU上

TIMED_WAITING: 代码中调用了sleep 或者 join(超时时间) 意思是当前的线程在一定时间之内,是阻塞状态,一定时间到了之后,阻塞状态解除

BLOCKED: 点前线程在等待锁,导致了阻塞 synchronized

WAITING:当前线程在等待唤醒,导致了阻塞 wait

线程安全

操作系统调度线程的时候是随机的

  • 如果因为这样的调度随机性引入了bug,就认为代码是线程不安全的
  • 如果没有带来bug,那么久认为是线程安全的

一个线程不安全的案例

使用两个线程,对同一个整形变量进行自增操作

  • 结果永远小于10000,并且大于5000 这就是最简单的线程不安全案例
    在这里插入图片描述

怎么解决这种安全问题呢

  1. 加锁(synchronized
    1. 通过加锁,把乱序的并发,变成一个串行操作
    2. 加锁之后,并发程度变低,数据更靠谱,但是速度更慢了;

加锁的方式

Java中加锁的方式有很多种 最常使用 synchronized 这样的关键字
在这里插入图片描述
给方法加上synchronized关键字,此时进入方法 就会自动加锁,离开方法 就会自动解锁

当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待(此时对应的线程,就出在BLOCKED状态)

阻塞会一直持续到占用锁的线程将锁释放为止

什么样的代码会产生线程不安全问题呢

首先: 不是所有的多线程代码都要加锁


产生线程不安全的原因

  1. 线程是抢占式执行的,线程间的调度充满随机性(线程不安全的根本原因)

  2. 多个线程对同一个变量进行修改操作,如果是多个线程针对不同变量进行修改,或者多个线程对同一个变量读也不会出现线程安全问题

  3. 针对变量的操作不是原子的

    1. 此处说的操作原子性类似数据库事务原子性
    2. 有些操作,比如读取变量的值,只是对应一跳机器指令,此时这样的操作就可以看做是原子的
    3. 通过加锁操作,就是把好几个指令给打包成一个原子的了
  4. 内存可见性,也会影响到线程安全

  5. 指令重排序,也会影响到线程安全问题

    1. 指令重排序,也是编译器优化中的一种操作
    2. 使用synchronized关键字

内存可见性导致的线程不安全问题实例
在这里插入图片描述

解决内存不安全导致线程不安全的问题
  1. 使用==synchronized== 关键字

    1. 不光可以保证执行的原子性,同时也能保证内存可见性
  2. 使用==volatile==关键字

    1. volatile和原子性无关,但是能够保存内存可见性
    2. 禁止编译器做出上述优化,编译器每次执行 都会从内存中重新读取
  3. 加上sleep编译器优化就消失了,也没有内存可见性问题了
    在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值