java并发编程(2) java线程2


前言

这一系列基于黑马的视频:java并发编程,目前还没有看完,整体下来这是我看过的最好的并发编程的视频。下面是根据视频做的笔记。


1、sleep和yield

1、sleep+interrupt

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
  3. 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
  4. 建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性
@Slf4j
public class Test5 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                log.debug("enter running......");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    log.debug("wake up.........");
                    e.printStackTrace();
                }
            }
        };
        //t1线程运行
        t1.start();

        try {
            //主线程睡眠1秒
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("interrupt..........");
        //打断t1,此时抛出异常
        t1.interrupt();
    }
}

可读性:

//主线程睡眠1秒
TimeUnit.SECONDS.sleep(1);

2、yield

  1. 调用 yield 会让当前线程从 Running(运行状态) 进入 Runnable 就绪状态,然后调度执行其它线程,等待有空闲线程再执行。
  2. 具体的实现依赖于操作系统的任务调度器 ,值得注意的是,有时候在线程少的情况下,还是要靠多线程的优先级进行判定,不是说调用了这个方法就一定可以获取到CPU的使用权。



2、线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间
  • 但 cpu 闲时,优先级几乎没作用
public final void setPriority(int newPriority) {
     ThreadGroup g;
     checkAccess();
 	//MAX_PRIORITY:10 		MIN_PRIORITY:1
     if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
         throw new IllegalArgumentException();
     }
     if((g = getThreadGroup()) != null) {
         if (newPriority > g.getMaxPriority()) {
             newPriority = g.getMaxPriority();
         }
         setPriority0(priority = newPriority);
     }
 }
public class Test6 {
    public static void main(String[] args) {
        Runnable task1 = new Runnable() {
            int count1 = 0;
            @Override
            public void run() {
                for(; ;){
                    //Thread.yield();
                    //线程1让给线程2
                    System.out.println("===>1 ==>" + count1 ++);
                }
            }
        };

        Runnable task2 = new Runnable() {
            int count2 = 0;
            @Override
            public void run() {
                for(; ;){
                    System.out.println("===>2 ==>" + count2 ++);
                }
            }
        };

        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        t2.start();
    }
}

上面的线程在线程数不足的时候优先级高的先执行。



3、常见方法

1. sleep

在这里插入图片描述

2. join

线程等待,调用此方法的线程会等待其他线程结束自己才结束。

@Slf4j
public class Test7 {
    static int r = 0;
    public static void main(String[] args) throws Exception {
        test1();
    }

    private static void test1() throws Exception {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        //主线程等待线程t1结束
        t1.join();
        log.debug("结果为:{}", r);//10,主线程等t1线程先运行结束
        log.debug("结束");
    }
}

在这里插入图片描述
下面演示了两个线程之间进行join:可以看到的是,t1和t2进行join之后,主线程等待t1睡眠1秒,再等待t2睡眠1秒,此时就继续执行。注意这里的t2在t1睡眠时间结束后只需要睡眠1秒,相当于t1.joi()和t2.join()是同步执行的。

@Slf4j
public class Test8 {
    static int r1 = 0;
    static int r2 = 0;

    public static void main(String[] args) throws InterruptedException {
        test2();
        //DEBUG [main] (15:49:23,996) (Test8.java:44) - join begin....
        //DEBUG [main] (15:49:25,004) (Test8.java:46) - t1 join end....
        //DEBUG [main] (15:49:26,010) (Test8.java:48) - t2 join end....
        //DEBUG [main] (15:49:26,013) (Test8.java:50) - r1:10, r2:20, time:-2015
    }

    public static void test2() throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        }, "t1");

        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r2 = 20;
        }, "t2");

        t1.start();
        t2.start();

        long start = System.currentTimeMillis();
        log.debug("join begin....");
        t1.join();
        log.debug("t1 join end....");
        t2.join();
        log.debug("t2 join end....");
        long end = System.currentTimeMillis();
        log.debug("r1:{}, r2:{}, time:{}", r1, r2, start - end);
    }
}

在这里插入图片描述
当然,join也可以指定时间,但是要注意,例如下面这个程序,join指定了时间,但是指定的时间小于线程睡眠的时间,所以线程还没运行完成,r1并没有赋值成功主线程就已经结束整个程序了。
在这里插入图片描述

3. interrupt

1. 打断 sleep,wait,join 的线程

首先通过:中断详细讲解这篇文章了解interrupt interrupted isInterrupted 这三个方法起到什么作用。

sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态。

下面两个程序要体现的一个结论就是:打断这些特殊的线程,会导致抛出异常,然后打断状态由true恢复成false

@Slf4j
public class Test9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            log.debug("sleep....");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted());
    }
    //DEBUG [t1] (00:13:48,827) (Test9.java:17) - sleep....
    //DEBUG [main] (00:13:49,835) (Test9.java:27) - interrupt
    //DEBUG [main] (00:13:49,836) (Test9.java:29) - 打断标记:false
    //一秒后打断
    //如果是sleep等被打断,就会抛异常,如果是正常线程被打断,就没有什么问题。
}
@Slf4j
public class Test11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                log.debug("线程任务执行");
                try {
                    Thread.sleep(10000); // wait, join
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    log.debug("被打断");
                }
            }
        };
        t1.start();
        Thread.sleep(500);
        log.debug("111是否被打断?{}",t1.isInterrupted());
        t1.interrupt();//打断线程,会导致睡眠的线程抛出异常,这时候打断标志会被重新设置为false
        log.debug("222是否被打断?{}",t1.isInterrupted());
        Thread.sleep(500);
        log.debug("222是否被打断?{}",t1.isInterrupted());
        log.debug("主线程");
    }
}



2. 打断正常线程

打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();的返回值为true,可以判断Thread.currentThread().isInterrupted();的值来手动停止线程

总结一句话就是打断正常的线程,只是设置了一个标记位,真正要做的是看你对这个标记位做了什么。java中打断阻塞的线程的时候就对这个标记位进行异常抛出,而正常的线程没做任何处理。


@Slf4j
public class Test10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(true){
                //判断boolean值,被打断后就会变成真
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted) {
                    log.debug("被打断了, 退出循环");
                    break;
                }
            }
        }, "t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        //DEBUG [main] (00:26:14,453) (Test10.java:28) - interrupt
        //DEBUG [t1] (00:26:14,457) (Test10.java:20) - 被打断了, 退出循环
    }

}

3. 多线程设计模式:两阶段终止模式

Two Phase Termination,其实就是让一个线程在被打断地时候可以优雅的完成正在进行的工作然后再结束,针对这个特点,很容易想到使用interrupt,因为这种方法在中断的时候是通过设置标记来进行的,但同时也要注意下面这几种错误的方法思路:

错误思路:
在这里插入图片描述
如下所示:线程的isInterrupted()方法可以取得线程的打断标记,用一个while死循环判断有没有被打断,如果有,就料理后事,结束循环,如果没有就睡眠,如果在睡眠过程中有异常了就设置一个打断标记,因为在睡眠中被中断此时是不会设置打断标记的。
简单来说就是无论什么情况,只要有异常或者正常的中断,我们都对他进行一个后事的处理。
在这里插入图片描述

下面的程序过程就是一开始不断循环睡眠判断,然后在3秒的时候主程序向下执行,调用interrupt方法进行打断,这时候就会抛出异常并把标记设置为true,然后下一次循环就会料理后事,退出while

@Slf4j
public class Test12{
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination t = new TwoPhaseTermination();
        t.start();

        Thread.sleep(3000);
        t.stop();
    }
}

@Slf4j
class TwoPhaseTermination{
    private Thread monitor;

    //启动监控线程
    public void start(){
        monitor = new Thread(()->{
            while(true){
                //获取当前线程
                Thread thread = Thread.currentThread();
                //判断是否被打断了
                if(thread.isInterrupted()){
                    //如果被打断了,就执行后面的代码
                    log.debug("料理后事");
                    break;
                }
                //没被打断就睡一秒
                try {
                    Thread.sleep(1000); //情况1,睡眠其中被打断了标志位就会设置为false
                    log.debug("执行监控记录");    //情况2
                } catch (InterruptedException e) {
                    //重新设置打断标志为true
                    e.printStackTrace();
                    //设置标志位为true
                    thread.interrupt();
                }
            }
        });

        monitor.start();
    }

    //停止监控线程
    public void stop(){
        monitor.interrupt();
    }
    //DEBUG [Thread-0] (19:45:33,808) (Test12.java:41) - 执行监控记录
	//DEBUG [Thread-0] (19:45:34,825) (Test12.java:41) - 执行监控记录
	//java.lang.InterruptedException: sleep interrupted
	//at java.lang.Thread.sleep(Native Method)
	//at com.jianglianghao.HeiMaJUC.test.TwoPhaseTermination.lambda$start$0(Test12.java:40)
	//at java.lang.Thread.run(Thread.java:748)
	//DEBUG [Thread-0] (19:45:35,810) (Test12.java:36) - 料理后事
}

4. 打断 park 线程

  • 调用interrupt()方法打断 park 线程, 不会清空打断状态
  • 如果打断标记已经是 true, 则 park 会失效,线程不会暂停
  • 可以使用 Thread.interrupted() 清除打断状态
@Slf4j
public class test13 {
    public static void main(String[] args) throws Exception {
        test();
    }

    public static void test() throws Exception{
        Thread t1 = new Thread(()->{
            log.debug("park.....");
            //暂停当前线程
            LockSupport.park();
            log.debug("unpark.........");
            //Thread.interrupted():返回当前的打断状态而且设置为假
            log.debug("打断状态:{}", Thread.interrupted());
            //当打断标记为真的时候park就失效,假的时候才可以生效
            LockSupport.park();
            log.debug("unpark.........");
            //DEBUG [t1] (20:09:42,328) (test13.java:23) - park.....
            //DEBUG [t1] (20:09:45,335) (test13.java:26) - unpark.........
            //DEBUG [t1] (20:09:45,338) (test13.java:28) - 打断状态:true
        }, "t1");
        t1.start();

        Thread.sleep(3000);
        t1.interrupt();

    }
}


5. 不推荐方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名static功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

6. 主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。下面是黑马给出的例子:

可以看到主线程结束了,守护线程尽管还在sleep,仍然会结束。

	log.debug("开始运行...");
	Thread t1 = new Thread(() -> {
	log.debug("开始运行...");
	sleep(2);
	log.debug("运行结束...");
	}, "daemon");
	// 设置该线程为守护线程
	t1.setDaemon(true);
	t1.start();
	sleep(1);
	log.debug("运行结束...");

//08:26:38.123 [main] c.TestDaemon - 开始运行... 
//08:26:38.213 [daemon] c.TestDaemon - 开始运行... 
//08:26:39.215 [main] c.TestDaemon - 运行结束...

注意:

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求,而是直接结束程序

7. 五种状态

从操作系统的层面来看这五种状态:
在这里插入图片描述

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

  • 【运行状态】指获取了 CPU 时间片运行中的状态

     当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
    
  • 【阻塞状态】
    1、 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
    2、 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    3、 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态


8. 六种状态

从java的API层面来看线程的6种状态,根据 Thread.State 枚举,分为六种状态
在这里插入图片描述

  • NEW 线程刚被创建,但是还没有调用 start() 方法

  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统
    层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)

  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分

  • TERMINATED 当线程代码运行结束


9. 统筹规划

阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案,提示

  • 用两个线程(两个人协作)模拟烧水泡茶过程
  • 文中办法乙、丙都相当于任务串行而图一相当于启动了 4 个线程,有点浪费
  • 用 sleep(n) 模拟洗茶壶、洗水壶等耗费的时间

附:华罗庚《统筹方法》

统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。
怎样应用呢?主要是把工序安排好。

比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?

  • 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
  • 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
  • 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。

哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。
这是小事,但这是引子,可以引出生产管理等方面有用的方法来。

水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:
在这里插入图片描述
从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大可利用“等水开”的时间来做,合并之后:
在这里插入图片描述

代码的简单实现,烧开水时间换成5s:

@Slf4j
public class Test14 {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            log.debug("洗水壶");
            sleep.mySleep(1);
            log.debug("烧开水");
            sleep.mySleep(5);
        }, "老王");

        t1.start();

        Thread t2 = new Thread(()->{
            log.debug("洗茶壶");
            sleep.mySleep(1);
            log.debug("洗茶杯");
            sleep.mySleep(2);
            log.debug("拿茶叶");
            sleep.mySleep(1);
            try {
                //等开水烧开
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("老王水烧开了,泡茶");
        }, "小王");

        t2.start();
    }
    //DEBUG [老王] (12:22:06,874) (Test14.java:19) - 洗水壶
    //DEBUG [小王] (12:22:06,874) (Test14.java:28) - 洗茶壶
    //DEBUG [老王] (12:22:07,886) (Test14.java:21) - 烧开水
    //DEBUG [小王] (12:22:07,886) (Test14.java:30) - 洗茶杯
    //DEBUG [小王] (12:22:09,888) (Test14.java:32) - 拿茶叶
    //DEBUG [小王] (12:22:12,887) (Test14.java:40) - 老王水烧开了,泡茶
}



4、 资源限制的挑战

这部分是《java并发编程的艺术》这本书提出的,对于一些现实地方的线程资源消耗的解决也有很大的意义,因此贴出来说说这部分(资料来自于这本书)。

1. 什么是资源限制

按照《java并发编程的艺术》这本书的说法。资源限制是指进行并发编程的时候,程序的执行速度受限于计算机硬件资源或软件资源。例如:服务器的带宽只有2MB/s,某个资源下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载资源不会编程10Mb/s。
硬件资源的限制:

  • 带宽的上传/下载速度
  • 硬盘读写速度
  • CPU的处理速度

软件资源的限制:

  • 数据库的连接
  • socket的连接数

2. 资源限制引发的问题

并发编程中,程序运行加快的原因是将代码中串行执行的部分变成了并行执行,而最坏的情况就是由于资源的限制,仍然在串行执行 。这时候程序运行速度不仅没有加快,而是会减慢,因为此时增加了线程上下文切换带来的时间以及资源调度的时间。
比如:在CPU利用率达到100%的时候,CPU无法调动线程来执行任务,这时候的速度不会加快,反而会减慢。当然在下载大文件的时候,如果配合多线程在CPU充足的情况下去下载,此时下载速度会显著提升。


3. 如何解决

  • 对于硬件资源,可以使用集群并行执行程序。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。
  • 对于软件资源, 可以考虑用资源池将资源复用。比如用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据的时候只建立一个连接。

4. 在资源限制情况下进行并发编程

在资源限制的情况下,我们要根据不同的资源限制调整程序的并发度

  • 下载文件程序依赖两个资源-带宽和硬盘读写速度‘
  • 数据库涉及数据库连接数时,如何SQL语句执行非常快。而线程的数量比数据库连接数的数量大得多,那么某些线程就会被阻塞,无法得到充分利用
  • 在不同情况下,根据此时的情况去适当调整线程的数量
  • 线程池技术(*****)






如有错误,欢迎指出

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值