JAVA并发编程梳理与学习一(线程基础概念解读)

引言:编程3年多了,感到自己知识体系零散,把自己知识体系梳理和学习一下。欢迎大家提意见,共同学习。
并发编程知识体系:线程基础概念解读、线程之间的共享和协作、线程并发工具类、原子操作CAS、显示锁和AQS、并发容器、线程池、并发安全、JVM、垃圾回收
一、进程和线程的定义
进程:操作系统进行资源(cpu、内存、磁盘I/O等)分配的最小单位。当你运行一个程序,你就启动了一 个进程,是活的,应用程序是死的。进程和进程之间是相互独立的,进程是系统进行资源分配和调度的一个独立单位。每一个进程都有它自己的地址空间,由程序、数据和进程控制块三部分组成。进程可以分为系统进 程和用户进程。
线程:cpu调度的最小单位。不能独立于进程存在,他是比线程更小的、能独立运行的基本单位。一个进程可以有多个线程,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),能共享进程资源(会有线程安全问题),进程是不执行任务的,真正执行任务的是线程。
:任何一个程序都必须要创建线程,特别是 Java 不管任何程序都必须启动一个 main 函数的主线程; Java Web 开发里面的定时任务、定时器、JSP 和 Servlet等,任何一个监听事件, onclick的触发事件等都离不开线程和并发的知识
二、CPU核心数和线程数的关系
windows系统cpu内核数、逻辑处理器数
一般内核数和我们线程数是1:1关系,一个cpu内核可以执行一个线程,但是intel采用超线程技术使其变成1:2关系。那么问题来了,我们平时使用电脑好像并没有受到cpu核心数的限制,我们可以开远远大于逻辑处理器数的线程数?那就要说一下,cpu的时间片轮转机制。
三、cpu的时间片轮转机制(RR调度)
人的反应时间:人的反应时间最快为0.2秒左右。一般来说,经过训练的运动员应该也不会低于0.1秒。
一个1.6G的cpu执行一条指令:大约0.6ns
通过对比,在人反应时间内,cpu大约能执行大约百万到千万的指令,所以当我们比如说打开一个网页时间,cpu会把时间划分成很多很多片段,然后拿出其中一个片段用来打开网页,我们肉眼是分辨不出来的,所以我们感觉自己操作电脑好像没受cpu核心数的限制。所以是我们感觉欺骗了我们。
当然,cpu的时间片轮转机制也是有代价的,cpu在轮转时,要做上下文切换,是非常耗费性能的。
所以,我们编写代码时间要尽量减少因为编写代码的不合适引起的上下文切换
:假如进程切( processwitch),有时称为上下文切换( context switch),需要 10ms, 再假设时间片设为 100ms,则在做完 100ms 有用的工作之后,CPU 将花费 10ms 来进行 进程切换。CPU 时间的 10%被浪费在了管理开销上了。
为了提高 CPU 效率,我们可以将时间片设为 5000ms。这时浪费的时间只有 0.1%。但考虑到一个9个线程系统中,如果有 10 个交互用户几乎同时按下回车键, 将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待 5s 才获得运行机会。多数用户无法忍受一条简短命令要 5 s才能做出响应。 所以从上面可以看出时间片设得太短会导致过多的上下文切换,降低了 CPU 效率;而设得太长又可能引起对用户交互请求的响应变差,降低系统和用户体验效果。将时间片设为 100ms通常是一个比较好的。
四、并发和并行
并行:可以同时运行的任务数,确确实实的同时执行
并发:交替执行,但是我们肉眼发觉不到,感觉也是同时执行(上面说的cpu的时间片轮转机制)
举个例子:我们排队打饭,要是排2队在一个窗口打饭,就是并发。排2队在2个窗口打饭就是并行
:当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少? 离开了单位时间其实是没有意义的。
五、高并发编程的意义、好处和注意事项
1.可以充分利用cpu的资源(比如:我们机器可以并行执行8个线程,我们要是只写一个单线程程序,那么其余7个cpu就不可以充分利用起来)
2.加快相应用户时间(比如:迅雷下载,开多个下载总归比开一个下载快)
3.可以使我们的代码模块化、异步化、简单化(比如:电商平台,我们下订单后,先减库存,在发订单,发短信,发邮件,如果用并行那么,整个流程就是时间相加。如果用并发,我们可以把他们交给不同模块区同时去执行)
有优点就会有缺点,毕竟没有十全十美的。
缺点
1.线程安全问题(共享资源),会引入锁(后面会提到),但是可能出现死锁、会有线程之间竞争,在竞争中可能会性能反而下降,甚至还不如单线程的速度快,不要认为多线程一定会比单线程快,如果写不好,还不如单线程。
注意:只读是没有线程安全问题的,之所以发生线程安全问题,是不同线程对共享的变量进行了写的操作。
死锁:不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成,产生死锁问题。比如:A和B要完成作业,A有笔,B有本子。A拿着笔会去等待本子,B拿着本子会去等待A的笔,结果就是A和B会一直等待下去,进入死锁状态。
2.线程是可以提高速度,但是并不意味着可以无限去创建线程(os限制,每创建一个线程jvm要分配栈空间,不调整参数大约1M,每创建一个线程会消耗资源,有可能把服务器搞死,所以会有线程池)
五、java里的线程
java中启动线程的方式
有的人说有3种,有的说有2种,我查了jdk源码,源码注释说有2种
在Thread类第73行有说明
1.继承Thread类
1】定义Thread类的子类,并重写该类的run()方法,run()方法也称为线程执行体。
2】启动线程,即调用线程的start()方法

public class MyThread extends Thread{//继承Thread类
        @Override
        public void run(){
            //重写run方法
        }
    public static void main(String[] args){
        new MyThread().start();

    }
}

2.实现Runnable接口(把Callable和Future创建线程归到这类里面)
1】定义Runnable接口的实现类,重写run()方法。
2】创建Runnable实现类的实例,并用这个实例作为Thread的参数来创建Thread对象,这个Thread对象才是真正的线程对象
3】调用线程对象的start()方法来启动线程

public class MyRunnable implements Runnable {//实现Runnable接口
    @Override
    public void run() {
        //重写run方法
    }

    public static void main(String[] args){

        MyRunnable myThread=new MyRunnable();

        Thread thread=new Thread(myThread);

        thread.start();
       //或者
       new Thread(new MyRunnable()).start();
    }
}

Callable和Future创建线程
1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public class CallableTest {

    public  static void main(String[] args){

        CallableTest callableTest=new CallableTest();
        FutureTask<Integer> future=new FutureTask<Integer>(
                (Callable<Integer>)()->{ return 5;}
                );

       new Thread(future,"有返回值的线程").start();

        try{
            System.out.println("子线程返回值 : " + future.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

2种创建方法的实质及优缺点
Thread是真正对线程的抽象,Runnable是对任务的抽象或者说是对业务逻辑的抽象。从上面代码也可以看出,最终启动都需要Thread来启动
1.采用实现Runnable、Callable接口的方式创建多线程:
优势:
(1)线程类只是实现了Runnable接口与Callable接口,还可以继承其他类。
(2)在这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:
编程稍稍复杂,如果需要访问当前线程,则要用Thread.currentThread()方法。
2.采用继承Thread类的方法创建多线程:
劣势:因为线程类已经继承了Thread类,所以不能再继承其他父类。
优势:编写简单,如果需要访问当前线程,用Thread.currentThread()方法,直接使用this即可获得当前线程
六、线程终结方法解读
1.suspend(暂停)、resume(恢复) 和 stop(停止)方法,jdk已废弃,不建议使用。
原因:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方 法
2.interrupt、static的interrupted、isInterrupted
jdk里面线程是协作式,不是抢占式
interrupt是设置线程一个中端标志位,被中断的线程不一定要立即停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。
isInterrupted和static的interrupted都可以测试线程是否中端,但是有区别。看源码
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
1》 interrupted会清除中断位,isInterrupted则不会
2》interrupted()方法为Thread类的static方法,而isInterrupted()方法不是;
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在 这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即 将线程的中断标示位清除,即重新设置为 false。
不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调 用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取 消标志。
这种情况下,使用中断会更好。
一、一般的阻塞方法,如 sleep 等本身就支持中断的检查。
二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态可 以避免声明取消标志位,减少资源的消耗。 注意:处于死锁状态的线程无法被中断
为什么阻塞方法会重置标志位?因为如果不重置标志位,如果检测到中断后马上中断程序,就会和调上面stop等方法一样,有些资源不会释放,没有给我们手动干预释放资源的时间
七、理解run和start方法
先看start源码
start源码描述
我们创建线程时,会先new一个线程对象,然后通过start方法来和操作系统交互,所以start是启动一个线程。
而run方法一般写业务逻辑,只是一个java方法,在哪个线程调用就是实现哪个线程业务逻辑。
八、线程生命周期
在这里插入图片描述
状态解读:
新建:使用new方法,new出来的线程;
就绪:调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
运行:就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
死亡:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
主要方法解读
yield():使当前线程让出 CPU 占有权。不会释放锁资源。 所有执行 yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
wait()/notify()/notifyAll():后面再说
join:把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行
线程的优先级
感觉没啥用。
通过setPriority(1-10)方法来修改优先级。在不同的 JVM 以及操作系统上,线程规划会 存在差异,有些操作系统甚至会忽略对线程优先级的设定,所以感觉没啥用,我是从没用过。
守护线程
定义:Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
注意:当jvm中不存在非守护线程的时候,守护线程也将停止。守护线程finally也不一定起作用,所以在构建 Daemon 线程时,不能依靠 finally 块中 的内容来确保执行关闭或清理资源的逻辑。
设置方法:Thread.setDaemon(true),比如垃圾回收线程就是 Daemon 线程。
注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章