《Java并发编程基础》第四章Java并发编程基础笔记

1.什么是线程

这个问题,大厂大概率都会问到(线程和进程的区别)。
现代操作系统再运行一个程序时都会为其创建一个进程,它是操作系统资源分配最小的单位,而进程可创建多个线程,线程是现代操作系统调度的最小单位。
详细见此文
Java程序是个天生的多线程。可由下面程序打印线程信息。

public class MultiThread {
    public static void main(String[] args) {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
        for(ThreadInfo threadInfo:threadInfos){
            System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName());
        }
    }
}

在这里插入图片描述

为啥需要使用多线程?
  • (硬件条件)随着核心数量越来越多和超线程技术的广泛应用。计算机并行计算能力更强了。
  • (需要)当业务复杂时,为了更良好的用户体验,需要更快的相应速度(比如之前写那个发邮件的demo,总不能让用户傻等邮件发完再进行下一步无关操作吧)。
  • (软件条件)Java为多线程编程提供了良好、考究并且一致的编程模型。
线程优先级

现代操作系统一般采用时分的形式调度运行的线程,每个线程都会分配到若干的时间片,用完了则会重新分配,优先级越高,分得的时间片越多。频繁阻塞的线程应该分配到更的优先级,偏计算(长时间占用CPU) 的线程则设置较的优先级。有些操作系统会忽视堆优先级的设定(比如书中的Ubuntu14.0.4,Mac OS X10.10),不过俺win10+Jdk11并不会。

public class Priority {
    private static volatile boolean notStart = true;
    private static volatile boolean notEnd  = true;
    public static void main(String[] args) throws InterruptedException {
        List<Job> jobs = new ArrayList<>();
        for(int i=0;i<10;i++){
            int priorty = i<5?Thread.MAX_PRIORITY:Thread.MIN_PRIORITY;
            Job job = new Job(priorty);
            jobs.add(job);
            Thread thread = new Thread(job,"Thread:"+i);
            thread.setPriority(priorty);
            thread.start();
        }
        notStart = false;
        TimeUnit.SECONDS.sleep(10);
        notEnd = false;
        for(Job job:jobs){
            System.out.println("Job Priority:"+job.priorty+"Job Count:"+job.jobCount);
        }
    }
    private static class Job implements Runnable{
        private int priorty;
        private long jobCount;

        public Job(int priorty) {
            this.priorty = priorty;
        }

        @Override
        public void run() {
            while(notStart){
                Thread.yield();
            }
            while (notEnd){
                Thread.yield();
                jobCount++;
            }
        }
    }
}

在这里插入图片描述
可见差得还是挺大的。

线程的状态
状态说明
NEW初始状态,线程构建 new Thread()…之类的
RUNNABLE运行状态,就绪状态和运行状态统称为运行状态
BLOCKED阻塞状态,阻塞于锁
WATING等待状态,等待其他线程通知
TIME_WAITING超时等待,可在指定的时间内自行返回
TERMINATED终止状态,表示当前线程已经执行完毕

在这里插入图片描述

//jps -l
//jstack pid
 public static void main(String[] args) {
 		//超时等待
        new Thread(new TimeWaiting(),"TimeWatingThread").start();
        //等待
        new Thread(new Waiting(),"WatingThread").start();
        //超时等待
		new Thread(new Blocked(),"BlockThread-1").start();
        //阻塞
        new Thread(new Blocked(),"BlockThread-2").start();
    }

    private static class TimeWaiting implements Runnable{

        @Override
        public void run() {
            while(true){
                SleepUtils.second(100);
            }
        }
    }

    private static class Waiting implements Runnable{

        @Override
        public void run() {
            while(true){
                synchronized (Waiting.class){
                    try {
                        Waiting.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private static class Blocked implements Runnable{

        @Override
        public void run() {
            synchronized (Blocked.class){
                while (true){
                    SleepUtils.second(100);
                }
            }
        }
    }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由此还可以看出,Thread.sleep()期间是不会释放锁的。

Deamon线程

是一种支持型线程,是个工具人(线程),用作程序中后台调度以及支持性工作,当JVM中不存在非Deamon线程时,JVM将会退出,所有的Deamon线程会被立即终止。可以通过设置将普通线程设置为Deamon线程,不过需要在启动前设置。

public class Deamon {
    public static void main(String[] args) {
        Thread thread = new Thread(new DeamonRunner(),"DeamonRunner");
        thread.setDaemon(true);
        thread.start();
    }
    private static class DeamonRunner implements Runnable{
        @Override
        public void run() {
            try {
                SleepUtils.second(10);
            }finally {
                System.out.println("俺好了");
            }

        }
    }
}

在这里插入图片描述

线程的启动与终止

初始化

初始化源码,总之继承了父线程的是否为Deamon,优先级,加载资源的contextCloader以及可继承的ThreadLocal。以及会分配一个唯一的ID。

private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        Thread parent = currentThread();//父线程即为创建它的线程
       /**
       * 省略中间线程组的获取安全检查等操作
       */
        this.group = g;
        this.daemon = parent.isDaemon();//是否为Deamon线程
        this.priority = parent.getPriority();//获取爸爸的优先级
        //获取爸爸的contextClassLoader
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        //
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        this.tid = nextThreadID();
    }
启动线程

start()方法,由当前线程告知JVM,孩子生了(调用start方法),快来接生,如果接生婆(线程规划器)有空的话。最好给孩子起个名字,出问题了,方便调整。
中断: 线程的一个标识位属性,表示一个运行中的线程是否被其他线程中断。当前线程调用某线程的interrupt()方法时,设置中断标志位。注意会抛出中断异常的的地方都会先将线程标志位清除。

public void interrupt() {
        if (this != Thread.currentThread()) {//如果不是我断我自己的话
            checkAccess();//检查权限
            // thread may be blocked in an I/O operation
            synchronized (blockerLock) {//获取该线程的锁
                Interruptible b = blocker;
                if (b != null) {
                    interrupt0();  // 设置标志位
                    b.interrupt(this);
                    return;
                }
            }
        }

        // set interrupt status
        interrupt0();
    }
suspend(),resume(),stop()

这仨是配套的方法,暂停,恢复,停止。不过由于无法保证资源的正常释放或者会造成死锁问题等原因,已被废弃。

安全的终止线程

不仅可以用中断进行停止线程,还可以使用volatile的标志位,来停止任务。

线程间的通信

在前几章提过两种通信方式,1.信号量,2.共享变量,Java就是后者。

volatile、synchronized

说到共享变量,就绕不开这俩关键字。
由于每个线程对共享变量都有一份缓存,如果都是读还好,就怕就即时的更新,大家手里的缓存就不是最新的了,怎么办呀?
使用volatile关键字啊,它使用内存屏障迫使缓存行无效,而需要去主内存读写,且不可重排序。
那如果是一系列操作后的结果呢?synchronized 啊,它保证一段代码或者一个方法的排他性和可见性。
其实这俩某种意义上是一个东西,不过volatile只保证一个变量读和写排他性和可见性,而synchronized则保证一段代码或者方法结果排他性和可见性。

详细描述下synchronized ,在同步代码块,则会在代码首尾分别用monitorenter、monitorexit指令来控制,而同步方法则会被ACC_SYNCHRONIZED修饰。
要想访问任意受synchronized保护的Object,必须先获取它的监视器,如果失败进入备胎池(同步队列),线程状态变成阻塞(BLOCKED),当的访问Object的前任(前驱,之前获得锁的线程)释放锁,则该释放操作唤醒被阻塞的同步队列的线程,使得它们重新尝试获取监视器。噢真是像极了爱情(不是)
在这里插入图片描述

等待通知机制

如果俺是面试官肯定会问为啥wait、notify、notifyAll方法都是在Object的方法而不是Thread的。
我觉得原因有

  • Object是所有类的父类
  • 锁的标记是在对象上的,这意味着所有对象都可以成为锁。而且获取释放锁的操作由上文所述本质上是获取它的监视器。
  • 对于wait方法而言,它只有持有某个对象时对于其他线程来说才有等待的意义。
  • 对于notify方法和notifyAll方法来说,它们唤醒的是持有过该对象的线程。继续执行时肯定会继续持有该对象。

就可以发现上述一系列的操作都是围绕着该对象进行的,而众所周知,Object是所有类,所以大家都能拥有该方法。

那再来个问题,为啥使用wait、notify、notifyAll方法实现等待通知机制,而不用刚才所说的设置一个 volatile共享标志位
使用标志位是不是就需要不断的查询判断?这样是会浪费资源的,那可以让线程睡一段时间再来查询,这样的确是减少了一些资源的消耗,不过在睡期间如果错过了标志位修改就又凉了。所以出于及时性和资源的节约,便使用wait、notify、notifyAll方法 。(不要自己造轮子)
wait、notify、notifyAll方法的使用注意:

  • 调用前提是,对调用对象加锁
  • 调用wait()方法会从RUNNING变为WATING
  • notidy和notifyAll调用后等待线程不会立即从wait()返回,需要notify和notifyAll释放锁之后才有机会从wait返回
  • notify方法将等待队列中的一个等待线程从等待队列中移动到同步队列中,而notifyAll方法则将等待队列中所有的线程全部移到同步队列中,被移动的线程状态由WAITING->BLOCKED

等待方遵循原则:

  1. 获取锁对象
  2. 如果条件不满足,调用对象wait()方法
  3. 条件满足后运行对应逻辑

通知方遵循原则:

  1. 获取对象的锁
  2. 改变条件
  3. 通知所有等待在对象上的线程

在这里插入图片描述

管道输入/输出流

它主要用于线程之间的数据传输,而传输的媒介是内存。
demo如下:

public class Piped {
    public static void main(String[] args) throws IOException {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        in.connect(out);//必须匹配连接
        Thread printThread = new Thread(new Print(in),"PrintThread");
        printThread.start();
        int rece;
        try{
            while((rece = System.in.read())!=-1){
                out.write(rece);
            }
        }finally {
            out.close();
        }
    }

    static class Print implements Runnable{
        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive;
            try{
                while ((receive = in.read())!=-1){
                    System.out.print((char)receive);
                }
            }catch (IOException e){
            }
        }
    }
}
Thread.join()

这里用到了等待通知机制。等待每个线程终止的前提是其前驱现成的终止,当前驱线程终止时才从join方法返回。

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {//不设置最长等待时间
            while (isAlive()) {//只要那些家伙还活着,本线程就等
                wait(0);
            }
        } else {
            while (isAlive()) {
            //双重判断,要么线程死完了,要么最长等待时间到了
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
ThreadLocal的使用

线程变量,可以当前线程为键,任意对象为值。这个结构被附带在线程上。其好处与线程绑定,与类或者方法都无关。

demo:

public class Profiler {
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<>(){
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };

    public static final void begin(){
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static final long end(){
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args) throws InterruptedException {
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(Profiler.end());
    }
}

实例

等待超时模式

为了解决在等待限定时间内,另一个线程得到结果的情况。
之前说到的等待通知机制就不能解决这个情况,这就需要加入超时等待。
比如说join方法的后段代码。

while (isAlive()) {
            //双重判断,要么线程死完了,要么最长等待时间到了
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }

线程池技术

第一章就提过频繁的上下文切换可能会导致多线程并不如单线程快。线程池技术就是为了解决这个问题的,不再将线程的创建交给用户,而是预先创建若干个线程,重复使用它们。
好处是:减少频繁的创建线程与销毁线程所带来的开销,面对过量任务的提交能够平缓的劣化。
书中给出了一个线程池的例子。主要用了等待通知机制,当工作者线程要么处于工作状态,要么就是等待状态,要么就被开除了(移除工作者线程组),当添加新的工作时,通知一个工作者线程来活了,工作者线程再从工作列表中取出一个工作,干完后再进入等待状态,等待下一次唤醒。这样就做到了统一管理线程的生死。啊感觉有一丝妙啊

//工作者
class Worker implements Runnable{
        private volatile boolean running = true;
        @Override
        public void run() {
            while(running){
                Job job = null;
                synchronized (jobs){
                    while(jobs.isEmpty()){
                        try{
                            jobs.wait();
                        }catch (InterruptedException e){
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                    job = jobs.removeFirst();
                }
                if(job!=null){
                    job.run();
                }
            }
        }
//通知者
 public void execute(Job job) {
        if(job!=null){
            synchronized (jobs){
                jobs.addLast(job);
                jobs.notify();
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值