精华内容
下载资源
问答
  • 1.1什么是进程:进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等,同一进程中多条线程共享该进程中全部系统资源,而进程和进程 之间是相互独立。 1.2什么是线程:线程是 CPU ...

        1.线程之间的共享和协作

    1.1什么是进程:进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程 之间是相互独立的。

    1.2什么是线程:线程是 CPU 调度的最小单位,必须依赖于进程而存在。

        2.并行和并发的概念:

         并行:同时执行不同的任务;并发:不能脱离时间单位,交替的执行不同的任务

        3.多线程的好处: 充分利用cpu资源;加快用户的响应时间

    注意:并不是无穷无尽的创建线程:OS限制:windows最大是2000个,Linux最大是1000个(不改内核的前提),每创建一个线程都要分配一个栈空间。

        4.创建线程的方式:只有两种Thread和Runnable:

    (jdk官方给出There are two ways to create a new thread of execution/有两种方式可以创建新的执行线程)

    Thread才是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行

        4.1线程的生命周期-(来源:Java核心知识点整理.pdf):


            在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞 (Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自 运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换 

     

    4.1.4.1. 新建状态(NEW) 当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配 内存,并初始化其成员变量的值 
    4.1.4.2. 就绪状态(RUNNABLE): 当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和 程序计数器,等待调度运行。 
    4.1.4.3. 运行状态(RUNNING): 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状 态。 
    4.1.4.4. 阻塞状态(BLOCKED): 阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。 直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状 态。阻塞的情况分三种:  
    等待阻塞 ( o.wait-> 等待对列 ) :
     运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue) 中。 
    同步阻塞 (lock-> 锁池 ) 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线 程放入锁池(lock pool)中。 
    其他阻塞 (sleep/join) 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时, JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O 处理完毕时,线程重新转入可运行(runnable)状态。 
    4.1.4.5. 线程死亡(DEAD) 线程会以下面三种方式结束,结束后就是死亡状态。 
    正常结束(下文会详细说)
     1. run()或call()方法执行完成,线程正常结束。 
    异常结束
     2. 线程抛出一个未捕获的Exception或Error。 
    调用 stop 3. 直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。 

    5.终止

      • 线程自然终止:

    要么是run执行完成了,要么是抛出了一个为处理的异常导致线程提前结束

      • Stop

    线程Thread中的api暂停、恢复、停止是过期的,不建议使用

    suspend():暂停,在调用后,线程不会释放已经占有的资源(比如锁),而是占着资源进入睡眠状态,这样容易引发死锁;

    resume():恢复

    stop():停止,在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

      • 中断

    安全的终止是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,中断好比其他线程对该线程打了个招呼,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。因为Java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应,

       线程通过方法isInterrupted()来进行判断当前线程是否被中断,也可以调用静态方法Thread.interrupt()来进行判断当前线程是否被中断,不过Thread.interrupted() 会同时将中断标识位改写为 false。

    如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在 这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即 将线程的中断标示位清除,即重新设置为 false。

    不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调 用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取 消标志。这种情况下,使用中断会更好,因为,

    一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,

    二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可 以避免声明取消标志位,减少资源的消耗。

    注意:处于死锁状态的线程无法被中断

     

    深入理解run()和start()

    Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有和操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。

    Start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法,start()方法不能重复调用,如果重复调用会抛出异常。

    而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

    展开全文
  • 走进Java里线程世界

    多人点赞 2019-07-28 17:14:40
    线程基础、线程之间共享和协作基础概念什么是进程和线程CPU时间片轮转机制澄清并行和并发高并发编程意义、好处和注意事项认识Java里线程 基础概念 什么是进程和线程 进程是程序运行资源分配的最小单位 ...

    基础概念

    什么是进程和线程

    进程是程序运行资源分配的最小单位

    • 进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
    • 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。

    线程是CPU调度的最小单位,必须依赖于进程而存在

    • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
    • 线程无处不在。任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程; Java Web开发里面的定时任务、定时器、JSP和 Servlet、异步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都离不开线程和并发的知识。

    CPU核心数和线程数的关系

    • 多核心:也指单芯片多处理器( Chip Multiprocessors,简称CMP)。CMP是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理
    • 多线程: Simultaneous Multithreading,简称SMT。让同一个处理器上的多个线程同步执行并共享处理器的执行资源。
    • 核心数、线程数:目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成1:2的关系。1个CPU核心2个逻辑核心。

    查看计算机的逻辑核心数:

    System.out.println(Runtime.getRuntime().availableProcessors());
    

    CPU时间片轮转机制

            我们平时在开发的时候,感觉并没有受cpu核心数的限制,想启动线程就启动线程,哪怕是在单核CPU上,为什么?这是因为操作系统提供了一种CPU时间片轮转机制。

            时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

            百度百科对CPU时间片轮转机制原理解释如下:
            时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
            时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要一定时间的–保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切换(process switch) - 有时称为上下文切换(context switch),需要5毫秒,再假设时间片设为20毫秒,则在做完20毫秒有用的工作之后,CPU将花费5毫秒来进行进程切换。CPU时间的20%被浪费在了管理开销上。
            为了提高CPU效率,我们可以将时间片设为500毫秒。这时浪费的时间只有1%。但考虑在一个分时系统中,如果有十个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待5秒钟才获得运行机会。多数用户无法忍受一条简短命令要5秒钟才能做出响应。同样的问题在一台支持多道程序的个人计算机上也会发生。
            结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率;而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100毫秒通常是一个比较合理的折中。

            在CPU死机的情况下,其实大家不难发现当运行一个程序的时候把CPU给弄到了100%再不重启电脑的情况下,其实我们还是有机会把它kill掉的,我想也正是因为这种机制的缘故。

    澄清并行和并发

            当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少?离开了单位时间其实是没有意义的。
            俗话说,一心不能二用,这对计算机也一样,原则上一个CPU只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个CPU,也就是说只有一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”。

    综合来说:

    • 并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已。
    • 并行:指应用能够同时执行不同的任务。

    两者区别:一个是交替执行,一个是同时执行。
    并发和并行

    高并发编程的意义、好处和注意事项

    由于多核多线程的CPU的诞生,多线程、高并发的编程越来越受重视和关注。
    多线程可以给程序带来如下好处:

    1. 充分利用CPU的资源
              从上面的CPU的介绍,可以看的出来,现在市面上没有CPU的内核不使用多线程并发机制的,特别是服务器还不止一个CPU,如果还是使用单线程的技术做思路,明显就out了。因为程序的基本调度单元是线程,并且一个线程也只能在一个CPU的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力:如果是一个线程的程序的话,那是要浪费3/4的CPU性能:如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地利用CPU,减少CPU的空闲时间,发挥它的运算能力,提高并发量。
    2. 加快响应用户的时间
              比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应时间若提升1s,如果流量大的话,就能增加不少转换量。做过高性能web前端调优的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网站的响应速度。多线程,高并发真的是无处不在。
    3. 可以使你的代码模块化,异步化,简单化
              例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分,将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。
      多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体会它的魅力。

    多线程程序需要注意事项:

    1. 线程之间的安全性
              在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全。若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
    2. 线程之间的死锁
              为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。
      假如线程A获得了刀,而线程B获得了叉。线程A就会进入阻塞状态来等待获得叉,而线程B则阻塞来等待线程A所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。
    3. 线程太多了会将服务器资源耗尽形成死机当机
              线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?
              某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。

    认识Java里的线程

    Java程序天生就是多线程的
    一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。

    import java.lang.management.ManagementFactory;
    import java.lang.management.ThreadInfo;
    import java.lang.management.ThreadMXBean;
    
    /**
     *类说明:只有一个main方法的程序
     */
    public class OnlyMain {
        public static void main(String[] args) {
            //Java 虚拟机线程系统的管理接口
            ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            // 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
            ThreadInfo[] threadInfos =
                    threadMXBean.dumpAllThreads(false, false);
            // 遍历线程信息,仅打印线程ID和线程名称信息
            for (ThreadInfo threadInfo : threadInfos) {
                System.out.println("[" + threadInfo.getThreadId() + "] "
                        + threadInfo.getThreadName());
            }
        }
    }
    
    [6] Monitor Ctrl-Break //监控Ctrl-Break中断信号的
    [5] Attach Listener //内存dump,线程dump,类信息统计,获取系统属性等
    [4] Signal Dispatcher  // 分发处理发送给JVM信号的线程
    [3] Finalizer  // 调用对象finalize方法的线程
    [2] Reference Handler//清除Reference的线程
    [1] main //main线程,用户程序入口
    

    线程的启动与中止

    启动

    启动线程的方式
    1、X extends Thread;,然后X.start
    2、X implements Runnable;然后交给Thread运行

    /**
     *类说明:新启线程的方式
     */
    public class NewThread {
    	/*扩展自Thread类*/
    	private static class UseThread extends Thread{
    		@Override
    		public void run() {
    			super.run();
    			// do my work;
    			System.out.println("I am extended Thread");
    		}
    	}
    	/*实现Runnable接口*/
    	private static class UseRunnable implements Runnable{
    		public void run() {
    			// do my work;
    			System.out.println("I am implements Runnable");
    		}
    	}
    	
    
    	public static void main(String[] args){
    		new UseThread().start();
    		new Thread(new UseRunnable()).start();
    
    		new Thread(){
    			@Override
    			public void run() {
    				super.run();
    				// do my work;
    				System.out.println("I am extended Thread");
    			}
    		}.start();
    		new Thread(() -> {
    			// do my work;
    			System.out.println("I am implements Runnable");
    		}).start();
    		
    	}
    }
    
    
    I am extended Thread
    I am implements Runnable
    I am extended Thread
    I am implements Runnable
    

    Thread和Runnable的区别

    Thread才是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行。

    中止

    线程自然终止
    要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
    stop()
    暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
    中止
    安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。因为java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应。
    线程通过方法 isInterrupted() 来进行判断是否被中断,也可以调用静态方法 Thread.interrupted() 来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
    如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

    /**
     *类说明:阻塞方法中抛出InterruptedException异常后,如果需要继续中断,需要手动再中断一次
     */
    public class HasInterrputException {
    
    	private static class UseThread extends Thread{
    
    		public UseThread(String name) {
    			super(name);
    		}
    
    		@Override
    		public void run() {
    			while(!isInterrupted()) {
    				try {
    					Thread.sleep(500);
    				} catch (InterruptedException e) {//标志位设置为false
    					System.out.println(Thread.currentThread().getName()
    							+" in InterruptedException interrupt flag is "
    							+isInterrupted());
    					//资源释放
    //					interrupt();//标志位设置为true
    					e.printStackTrace();
    				}
    				System.out.println(Thread.currentThread().getName()
    						+ " I am extends Thread.");
    			}
    			System.out.println(Thread.currentThread().getName()
    					+" interrupt flag is "+isInterrupted());
    		}
    	}
    
    	public static void main(String[] args) throws InterruptedException {
    		Thread endThread = new UseThread("HasInterrputEx");
    		endThread.start();
    		Thread.sleep(1000);
    		endThread.interrupt();//中断线程,其实设置线程的标识位true
    	}
    }
    
    

    结果:
    结果
    如果将interrupt();这一行注释取消掉:
    结果
    不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,
    一、一般的阻塞方法,如sleep等本身就支持中断的检查。
    二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
    注意:处于死锁状态的线程无法被中断

    深入理解run()和start()

    Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。
    start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法,start()方法不能重复调用,如果重复调用会抛出异常。
    而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

    线程的生命周期

    线程的生命周期
    这里不是Java线程的6种状态。

    线程的优先级

    在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。
    设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

    线程的调度

    线程调度是指系统为线程分配CPU使用权的过程,主要调度方式有两种:

    • 协同式线程调度(Cooperative Threads-Scheduling):线程执行的时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。使用协同式线程调度的最大好处是实现简单,由于线程要把自己的事情做完后才会通知系统进行线程切换,所以没有线程同步的问题,但是坏处也很明显,如果一个线程出了问题,则程序就会一直阻塞。
    • 抢占式线程调度(Preemptive Threads-Scheduling):使用抢占式线程调度的多线程系统,每个线程执行的时间以及是否切换都由系统决定。在这种情况下,线程的执行时间不可控,所以不会有一个线程导致整个进程阻塞的问题出现。

    在Java中,Thread.yield()可以让出CPU执行时间,但是对于获取,线程本身是没有办法的。对于获取CPU执行时间,线程唯一可以使用的手段是设置线程优先级,Java设置了10个级别的程序优先级,当两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。

    Java中的线程是通过映射到操作系统的原生线程上实现的,所以线程的调度最终取决于操作系统,而操作系统级别,OS是以抢占式调度线程,我们可以认为线程是抢占式的。Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。而且操作系统中线程的优先级有时并不能和Java中的一一对应,所以Java优先级并不是特别靠谱。但是在Java中,因为Java没有提供安全的抢占式方法来停止线程,要安全的停止线程只能以协作式的方式。

    守护线程

    Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。
    Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

    线程间的共享

    synchronized内置锁

    线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
    Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

    对象锁和类锁

    对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
    但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。

    volatile,最轻量的同步机制

    volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

    /**
     * 类说明:演示Volatile的提供的可见性
     */
    public class VolatileCase {
        private  static boolean ready;
    //    private  static volatile boolean ready;
        private static int number;
    
        private static class PrintThread extends Thread{
            @Override
            public void run() {
                System.out.println("PrintThread is running.......");
                while(!ready);
                System.out.println("number = "+number);
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            new PrintThread().start();
            Thread.sleep(1000);
            number = 51;
            ready = true;
            Thread.sleep(5000);
            System.out.println("main is ended!");
        }
    }
    

    结果

    //    private  static boolean ready;
        private  static volatile boolean ready;
    

    在这里插入图片描述
    不加volatile时,子线程无法感知主线程修改了ready的值,从而不会退出循环,而加了volatile后,子线程可以感知主线程修改了ready的值,迅速退出循环。
    但是volatile不能保证数据在多个线程下同时写时的线程安全。该上锁的还是得上锁。
    volatile最适用的场景:一个线程写,多个线程读

    ThreadLocal辨析

    与Synchonized的比较

    ThreadLocal和Synchonized都用于解决多线程并发访问。

    • Synchronized:利用锁的机制,使方法或代码块在某一时该仅仅能被一个线程访问。
    • ThreadLocal:为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

    Spring的事务就借助了ThreadLocal类。Spring会从数据库连接池中获得一个connection,然会把connection放进ThreadLocal中,也就和线程绑定了,事务需要提交或者回滚,只要从ThreadLocal中拿到connection进行操作。

    为何Spring的事务要借助ThreadLocal类

    以JDBC为例,正常的事务代码可能如下:
    dbc = new DataBaseConnection();//第1行
    Connection con = dbc.getConnection();//第2行
    con.setAutoCommit(false);// //第3行
    con.executeUpdate(…);//第4行
    con.executeUpdate(…);//第5行
    con.executeUpdate(…);//第6行
    con.commit();第7行
    上述代码,可以分成三个部分:
    事务准备阶段:第1~3行
    业务处理阶段:第4~6行
    事务提交阶段:第7行
    可以很明显的看到,不管我们开启事务还是执行具体的sql都需要一个具体的数据库连接。
    现在我们开发应用一般都采用三层结构,如果我们控制事务的代码都放在DAO(DataAccessObject)对象中,在DAO对象的每个方法当中去打开事务和关闭事务,当Service对象在调用DAO时,如果只调用一个DAO,那我们这样实现则效果不错,但往往我们的Service会调用一系列的DAO对数据库进行多次操作,那么,这个时候我们就无法控制事务的边界了,因为实际应用当中,我们的Service调用的DAO的个数是不确定的,可根据需求而变化,而且还可能出现Service调用Service的情况。
    如果不使用ThreadLocal,代码大概就会是这个样子:
    在这里插入图片描述在这里插入图片描述
    但是需要注意一个问题,如何让三个DAO使用同一个数据源连接呢?我们就必须为每个DAO传递同一个数据库连接,要么就是在DAO实例化的时候作为构造方法的参数传递,要么在每个DAO的实例方法中作为方法的参数传递。这两种方式无疑对我们的Spring框架或者开发人员来说都不合适。为了让这个数据库连接可以跨阶段传递,又不显示的进行参数传递,就必须使用别的办法。
    Web容器中,每个完整的请求周期会由一个线程来处理。因此,如果我们能将一些参数绑定到线程的话,就可以实现在软件架构中跨层次的参数共享(是隐式的共享)。而JAVA中恰好提供了绑定的方法–使用ThreadLocal。
    结合使用Spring里的IOC和AOP,就可以很好的解决这一点。
    只要将一个数据库连接放入ThreadLocal中,当前线程执行时只要有使用数据库连接的地方就从ThreadLocal获得就行了。

    ThreadLocal的使用

    ThreadLocal类接口常用的4个方法:

    • void set(Object value)
      设置当前线程的线程局部变量的值。
    • public Object get()
      该方法返回当前线程所对应的线程局部变量。
    • public void remove()
      将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
    • protected Object initialValue()
      返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

    public final static ThreadLocal< String > RESOURCE = new ThreadLocal< String >();
    RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

    有示例如下:

    /**
     *类说明:演示ThreadLocal的使用
     */
    public class UseThreadLocal {
    	
    	private static ThreadLocal<Integer> intLocal
                = ThreadLocal.withInitial(() -> 1);
    
        /**
         * 运行3个线程
         */
        public void StartThreadArray(){
            Thread[] runs = new Thread[3];
            for(int i=0;i<runs.length;i++){
                runs[i]=new Thread(new TestThread(i));
            }
            for(int i=0;i<runs.length;i++){
                runs[i].start();
            }
        }
        
        /**
         *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
         */
        public static class TestThread implements Runnable{
            int id;
            public TestThread(int id){
                this.id = id;
            }
            public void run() {
                System.out.println(Thread.currentThread().getName()+":start");
                Integer s = intLocal.get();
                s = s+id;
                intLocal.set(s);
                System.out.println(Thread.currentThread().getName()
                        +":"+ intLocal.get());
            }
        }
    
        public static void main(String[] args){
        	new UseThreadLocal().StartThreadArray();
        }
    }
    
    

    结果:

    Thread-1:start
    Thread-2:start
    Thread-0:start
    Thread-1:2
    Thread-2:3
    Thread-0:1
    

    实现解析

    在这里插入图片描述

    看一下ThreadLocal的源码:

    public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    

    上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap,ThreadLocalMap 是ThreadLocal 的静态内部类,然后 Thread 类中有一个这样类型成员,所以 getMap 是直接返回Thread的成员。

    static class ThreadLocalMap {
            static class Entry extends WeakReference<ThreadLocal<?>> {
                Object value;
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    private Entry getEntry(ThreadLocal<?> key) {
                int i = key.threadLocalHashCode & (table.length - 1);
                Entry e = table[i];
                if (e != null && e.get() == key)
                    return e;
                else
                    return getEntryAfterMiss(key, i, e);
           }
    private void set(ThreadLocal<?> key, Object value) {
    		...
            }
    ...
    

    可以看到有个 Entry 内部静态类,它继承了 WeakReference ,是一个弱引用对象。Entry ThreadLocal 与副本对象,做成了 key-value 的形式。

    引发的内存泄漏分析

    预备知识

    引用
    Object o = new Object();
    这个o,我们可以称之为对象引用,而new Object()我们可以称之为在内存中产生了一个对象实例。
    当写下 o=null时,只是表示o不再指向堆中object的对象实例,不代表这个对象实例不存在了。

    • 强引用:指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
    • 软引用:用来描述一些还有用但并非必需的对象。仅软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
    • 弱引用:用来描述非必需对象的,但是它的强度比软引用更弱一些,仅被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
    • 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
    内存泄漏的现象

    示例如下:

    **
     * 类说明:ThreadLocal造成的内存泄漏演示
     */
    public class ThreadLocalOOM {
        private static final int TASK_LOOP_SIZE = 500;
    	//我们启用一个线程池,大小固定为5个线程
        final static ThreadPoolExecutor poolExecutor
                = new ThreadPoolExecutor(5, 5,
                1,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>());
    
        static class LocalVariable {
            private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
        }
    
        final static ThreadLocal<LocalVariable> localVariable
                = new ThreadLocal<>();
    
        public static void main(String[] args) throws InterruptedException {
            /*5*5=25*/
            for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
                poolExecutor.execute(new Runnable() {
                    public void run() {
                    	//首先只简单的在每个任务中new出一个数组
                        new LocalVariable();
                        //localVariable.set(new LocalVariable());
                        //localVariable.remove();
                    }
                });
    
                Thread.sleep(100);
            }
            System.out.println("pool execute over");
        }
    
    }
    

    并将堆内存大小设置为-Xmx256m:
    在这里插入图片描述
    结果:
    在这里插入图片描述
    可以看到内存的实际使用控制在25M左右:因为每个任务中会不断new出一个5M的数组,5*5=25M,这是很合理的。
    当我们启用了ThreadLocal以后:

    //new LocalVariable();
    localVariable.set(new LocalVariable());
    

    结果:
    在这里插入图片描述
    内存占用最高升至150M,一般情况下稳定在90M左右,那么加入一个ThreadLocal后,内存的占用真的会这么多?
    于是,我们加入一行代码:

    localVariable.remove();
    

    结果:
    在这里插入图片描述
    可以看见最高峰的内存占用也在25M左右,和我们不加ThreadLocal表现一样。
    这就充分说明,确实发生了内存泄漏。

    分析

    根据我们前面对ThreadLocal的分析,我们可以知道每个Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 Key 来让线程从 ThreadLocalMap 获取 value。仔细观察ThreadLocalMap,这个map是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
    因此使用了ThreadLocal后,引用链如图所示:
    在这里插入图片描述
    图中的虚线表示弱引用。
    这样,当把 ThreadLocalRef 变量置为 null 以后,只有一个虚引用引用指向 ThreadLocal 实例,所以Threadlocal 将会被 GC 回收。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的Entry 的 value 就会一直存在一条强引用链:Current Thread Ref -> Current Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永远不会被访问到了,所以存在着内存泄露。
    只有当前线程结束以后,Current Thread Ref 就不会存在栈中,强引用断开,Current Thread、Map、 Entry、value 将全部被 GC 回收。最好的做法是不在需要使用 ThreadLocal 变量后,都调用它的remove()方法,清除数据。
    其实考察ThreadLocal的实现,我们可以看见,无论是get()、set()在某些时候,调用了expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有remove()方法中显式调用了expungeStaleEntry方法。

    从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
    下面我们分两种情况讨论:
    key 使用强引用:ThreadLocalMap 一直持有 ThreadLocal 的强引用,如果没有手动置空这个强引用,ThreadLocal 的对象实例不会被回收,导致 ThreadLocal 和 Entry 一直不能被回收。
    key 使用弱引用:ThreadLocalMap 持有 ThreadLocal 的弱引用,ThreadLocal 的对象如果不再被强引用指向被回收。Entry 不能被回收,但在下一次 ThreadLocalMap 调用 set,get,remove 都有机会被回收。
    比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除 Entry ,都会导致内存泄漏,但是使用弱引用可以让 ThreadLocal 被回收。
    因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除 ThreadLocalMap 里的 Entry 就会导致内存泄漏,而不是因为弱引用。

    总结

    JVM利用设置 ThreadLocalMap 的 Entry 为弱引用对象,来避免 ThreadLocal 内存泄露。
    JVM利用调用 remove、get、set 方法的时候,回收弱引用对象 Entry,来避免 Entry 内存泄露 。
    当ThreadLocal存储很多 key 为 null 的 Entry 的时候,而不再去调用 remove、get、set 方法,那么将导致内存泄漏。
    使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了 value 可能造成累积的情况。

    关于弱引用弱引用对象的区别,请看 有关“引用”、“对象”、“引用对象”的误解和看过源码说明后的理解

    错误使用ThreadLocal导致线程不安全

    示例如下:

    /**
     * 类说明:ThreadLocal的线程不安全演示
     */
    public class ThreadLocalUnsafe implements Runnable {
    
        public static Number number = new Number(0);
    
        public void run() {
            //每个线程计数加一
            number.setNum(number.getNum()+1);
            //将其存储到ThreadLocal中
            value.set(number);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //输出num值
            System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
        }
    
        public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
        };
    
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                new Thread(new ThreadLocalUnsafe()).start();
            }
        }
    
        private static class Number {
            public Number(int num) {
                this.num = num;
            }
    
            private int num;
    
            public int getNum() {
                return num;
            }
    
            public void setNum(int num) {
                this.num = num;
            }
    
            @Override
            public String toString() {
                return "Number [num=" + num + "]";
            }
        }
    
    }
    

    结果:

    Thread-3=5
    Thread-0=5
    Thread-1=5
    Thread-4=5
    Thread-2=5
    

    为什么每个线程都输出5而不是1?难道他们没有独自保存自己的Number副本吗?为什么其他线程还是能够修改这个值?
    仔细考察ThreadLocal和Thead的代码,我们发现ThreadLocalMap中保存的其实是对象的一个引用,这样的话,当有其他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有的对象引用所指向的同一个对象实例。这也就是为什么上面的程序为什么会输出一样的结果:5个线程中保存的是同一Number对象的引用(因为被static修饰了),在线程睡眠的时候,其他线程将num变量进行了修改,而修改的对象Number的实例是同一份,因此它们最终输出的结果是相同的。
    而上面的程序要正常的工作,应该的用法是让每个线程中的ThreadLocal都应该持有一个新的Number对象(最简单的方法是去掉static)。

    线程间的协作

    线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环检查变量是否符合预期在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。却存在如下问题:
    1)难以确保及时性。
    2)难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。

    等待/通知机制

    是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

    wait,notify/notifyAll

    • notify():
      通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
    • notifyAll():
      通知所有等待在该对象上的线程。
    • wait()
      调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回。需要注意,调用wait()方法后,会释放对象的锁
    • wait(long)
      超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
    • wait (long,int)
      对于超时时间更细粒度的控制,可以达到纳秒

    等待和通知的标准范式

    等待方遵循如下原则。
    1)获取对象的锁。
    2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
    3)条件满足则执行对应的逻辑。

     synchronized(对象){
     	while(条件不满足){
     	对象.wait();
    	 }
    	//业务逻辑 
     }
    

    通知方遵循如下原则。
    1)获得对象的锁。
    2)改变条件。
    3)通知所有等待在对象上的线程。

    synchronized(对象){
    	//业务逻辑,改变条件
    	对象.notify()/notifyAll();
    }
    

    在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法。进入wait()方法后,当前线程释放锁,在从wait()返回前,线程与其他线程竞争重新获得锁, 执行notify()系列方法的线程退出调用了notifyAll的synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

    notify和notifyAll应该用谁

    尽可能用notifyall(),谨慎使用notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

    示例如下:

    /**
     *类说明:测试wait/notifyAll
     */
    public class TestWN {
        private static Express express = new Express(0,Express.CITY);
    
        /*检查里程数变化的线程,不满足条件,线程一直等待*/
        private static class CheckKm extends Thread{
            @Override
            public void run() {
            	express.waitKm();
            }
        }
    
        /*检查地点变化的线程,不满足条件,线程一直等待*/
        private static class CheckSite extends Thread{
            @Override
            public void run() {
            	express.waitSite();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            for(int i=0;i<3;i++){
                new CheckSite().start();
            }
            for(int i=0;i<3;i++){
                new CheckKm().start();
            }
    
            Thread.sleep(1000);
            express.changeKm();
            Thread.sleep(5000);
            System.out.println("***********************");
            express.changeSite();
        }
    }
    
    
    /**
     *类说明:快递实体类
     */
    public class Express {
        public final static String CITY = "ShangHai";
        private int km;/*快递运输里程数*/
        private String site;/*快递到达地点*/
    
        public Express(int km, String site) {
            this.km = km;
            this.site = site;
        }
    
        /* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
        public synchronized void changeKm(){
            this.km = 101;
            notifyAll();
        }
    
        /* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
        public  synchronized  void changeSite(){
            this.site = "BeiJing";
            notifyAll();
        }
    
        /*线程等待公里的变化*/
        public synchronized void waitKm(){
            while(this.km<100){
                try {
                    wait();
                    System.out.println("Check Site thread["
                                    +Thread.currentThread().getId()
                            +"] is be notified");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("the Km is "+this.km+",I will change db");
        }
    
        /*线程等待目的地的变化*/
        public synchronized void waitSite(){
            while(this.site.equals(CITY)){//快递到达目的地
                try {
                    wait();
                    System.out.println("Check Site thread["+Thread.currentThread().getId()
                    		+"] is be notified");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("the site is "+this.site+",I will call user");
        }
    }
    
    

    结果:

    Check Site thread[17] is be notified
    the Km is 101,I will change db
    Check Site thread[16] is be notified
    the Km is 101,I will change db
    Check Site thread[15] is be notified
    the Km is 101,I will change db
    Check Site thread[14] is be notified
    Check Site thread[13] is be notified
    Check Site thread[12] is be notified
    ***********************
    Check Site thread[12] is be notified
    the site is BeiJing,I will call user
    Check Site thread[13] is be notified
    the site is BeiJing,I will call user
    Check Site thread[14] is be notified
    the site is BeiJing,I will call user
    

    join()

    在线程A中调用了线程B的 join() 方法,直到线程B执行完毕后,才会继续执行线程A。
    原理是A线程被B线程里的wait(0)方法阻塞了,直到B线程的任务执行结束,底层的cpp代码会调用notify_all方法唤醒A线程。

    源码:

     public final void join() throws InterruptedException {
            join(0);
        }
    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()) {
                //假如在A线程里调用了B线程的join方法,会阻塞调用者
                //在cpp里,一个线程的任务执行结束之后,会调用ensure_join方法
                //ensure_join方法里会调用notify_all方法
                    wait(0);
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }
    

    调用yield()、sleep()、wait()、notify()方法对锁有何影响

    • yield()
      使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield( )的线程不一定就会持有锁,我们完全可以在释放锁后再调用yield方法。
      所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
    • sleep()
      不会释放锁资源。
    • wait()
      会释放锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。
    • notify()
      对锁无影响,线程只有在syn同步代码执行完后才会自然而然的释放锁,所以notify()系列方法一般都是syn同步代码的最后一行。

    最大线程数

    内存上的限制

    在 Java 语言里, 当你创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统线程的内存用的不是 JVMMemory,而是系统中剩下的内存。

    能创建的线程数的具体计算公式如下:
    (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads

    MaxProcessMemory 一个进程的最大内存
    JVMMemory JVM 内存
    ReservedOsMemory 保留的操作系统内存
    ThreadStackSize 线程栈的大小

    CPU上的限制

    参考我的这篇博文:如何选择线程池最优线程数

    【并发编程】目录:

    【并发编程】之走进Java里的线程世界

    【并发编程】之学会使用线程的并发工具类

    【并发编程】之学会使用原子操作CAS

    【并发编程】之深入理解显式锁和AQS

    【并发编程】之一文彻底搞懂并发容器

    【并发编程】之Java面试经常会问到的线程池,你搞清楚了吗?

    【并发编程】之Java并发安全知识点总结

    【并发编程】之大厂很可能会问到的JMM底层实现原理

    展开全文
  • 目录线程基础、线程之间共享和协作什么是进程和线程CPU核心数和线程数关系CPU时间片轮转机制澄清并行和并发高并发编程意义、好处和注意事项多线程程序需要注意事项 什么是进程和线程 1. 进程是程序运行资源...

    线程基础、线程之间的共享和协作

    序号 内容 链接
    1 Java线程基础、线程之间的共享和协作(一) https://blog.csdn.net/baidu_36882394/article/details/106190594
    2 Java线程基础、线程之间的共享和协作(二) https://blog.csdn.net/baidu_36882394/article/details/106330990
    3 Java线程基础、线程之间的共享和协作(三) https://blog.csdn.net/baidu_36882394/article/details/106385341
    4 Java线程基础、线程之间的共享和协作(四) https://blog.csdn.net/baidu_36882394/article/details/106419197

    什么是进程和线程

    1. 进程是程序运行资源分配的最小单位

    进程是操作系统进行资源分配的最小单位,其中包括:CPU,内存空间、磁盘IO等,同一进程中的多条线程共享改进程中的全部系统资源,而进程和进程之间是相互独立的。
    进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的的一个独立单位。
    进程是程序在计算机上的一次执行活动。当你运行一个程序,你就自动了一个进程。显然程序是死的、静态的,进程是活的,动态的。进程可以分为系统进程和用户进程。凡是用户完成操作系统的各种功能的进程是系统进程,它们就是处于运行状态下的的操作系统本身,用户进程就是所有由你启动的进程。

    2. 线程是CPU调度的最小单位,必须依赖于进程而存在

    线程是进程的一个实体,是CPU调度和分配资源的最小单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点:在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程中的其他线程共享进程中的所拥有的全部资源。

    3. 线程无处不在

    任何一个程序都必须要创建线程,特别是Java程序,不管任何程序都不必须启动一个main函数的主线程;Java Web开发里面的定时任务、定时器、JSP、Servlet、异步消息处理机制、远程访问接口RM等,任何一个监听事件,onclick的触发事件等都离不开线程和并发的知识。

    CPU核心数和线程数的关系

    1. 多核心

    多核心也指单芯片多处理器(Chip Multiprocessors,简称CMP),CMP是由美国斯坦福大学提出的,其思想是将大规模饼并行处理器中的SMP(对称多处理器)集成到同一个芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行的运行程序是实现超高计算的一个主要方向,称为并行处理。

    2. 多线程

    Simultaneous Multithreading.简称 SMT.让同一个处理器上的多个线程同步执行并共享处理器的执行资源。

    3. 核心数和线程数之间的关系

    核心数、线程数:目前主流 CPU 都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也就是说四核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系

    在这里插入图片描述

    CPU时间片轮转机制

    我们平时在开发的时候,感觉并没有受 cpu 核心数的限制,想启动线程就启动线程,哪怕是在单核 CPU 上,为什么?这是因为操作系统提供了一种 CPU 时间片轮转机制。时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

    百度百科对 CPU 时间片轮转机制原理解释如下:
    如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
    时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切换( processwitch),有时称为上下文切换( context switch),需要 5ms, 再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来进行进程切换。CPU 时间的 20%被浪费在了管理开销上了。
    为了提高 CPU 效率,我们可以将时间片设为 5000ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有 10 个交互用户几乎同时按下回车键, 将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待 5s 才获得运行机会。多数用户无法忍受一条简短命令要 5 才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发生。
    结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了 CPU 效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常是一个比较合理的折衷。
    在 CPU 死机的情况下,其实大家不难发现当运行一个程序的时候把 CPU 给弄到了 100%再不重启电脑的情况下,其实我们还是有机会把它 kill 掉的,我想也正是因为这种机制的缘故。

    澄清并行和并发

    我们举个例子,如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车辆就是 8 辆此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可以并行运行。CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线程数就相当于并排可以通行的车道;而多个 CPU 就相当于并排有多条高速公路,而每个高速公路并排有多个车道。当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少?离开了单位时间其实是没有意义的。俗话说,一心不能二用,这对计算机也一样,原则上一个 PU 只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个 CPU,也就是说只有一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”。
    综合来说:
    并发:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.
    并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行
    两者区别:一个是交替执行,一个是同时执行.
    在这里插入图片描述在这里插入图片描述

    高并发编程的意义、好处和注意事项

    由于多核多线程的 CPU 的诞生,多线程、高并发的编程越来越受重视和关注。
    多线程可以给程序带来如下好处。
    (1)充分利用 CPU 的资源
    从上面的 CPU 的介绍,可以看的出来,现在市面上没有 CPU 的内核不使用多线程并发机制的,特别是服务器还不止一个 CPU,如果还是使用单线程的技术做思路, 明显就 out 了。因为程序的基本调度单元是线程,并且一个线程也只能在一个 CPU的一个核的一个线程上跑,如果你是个 i3 的 CPU 的话,最差也是双核心 4 线程的运算能力:如果是一个线程的程序的话,那是要浪费 3/4 的 CPU 性能:如果设计一个多线程的程序的话,那它就可以同时在多个 CPU 的多个核的多个线程上跑,可以充分地利用 CPU,减少 CPU 的空闲时间,发挥它的运算能力,提高并发量。
    就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是为了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍。这就是为什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的时候可以并发地去做几件事情,充分利用我们的时间,CPU 也是一样,也要充分利用。
    (2)加快响应用户的时间
    比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。
    我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应时间若提升 1s,如果流量大的话,就能增加不少转换量。做过高性能 web 前端调优的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网站的响应速度。多线程,高并发真的是无处不在。
    (3)可以使你的代码模块化,异步化,简单化
    例如我们实现电商系统,下订单和给用户发送短信、邮件等就可以进行拆分,将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体会它的魅力。

    多线程程序需要注意的事项

    (1)线程之间的安全性

    从前面的章节中我们可以看到,在同一个进程里面的多个线程的资源是共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,没有写操作,一般来说,这个全局变量是线程安全的。若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

    (2)线程之间的死锁

    为了解决线程之间的安全性,Java引入了锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。
    假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁(共享刀和共享叉的锁)。
    假如线程 A 获得了刀,而线程 B 获得了叉。线程 A 就会进入阻塞状态来等待获得叉,而线程 B 则阻塞来等待线程 A 所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。

    (2)线程过多会将服务器资源耗尽形成死机最终导致宕机

    线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?
    某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。多线程应用开发的注意事项很多,希望大家在日后的工作中可以慢慢体会它的危险所在。哈哈~

    展开全文
  • 什么是模块:本质就是.py文件,封装语句的最小单位。 自定义模块:实际上就是定义.py,其中可以包含:变量定义,可执行语句,for循环,函数定义等等,他们统称模块成员。 模块运行方式: 脚本方式:直接用解释器...
    1. 今日内容大纲

    自定义模块:

    什么是模块:本质就是.py文件,封装语句的最小单位。

    自定义模块:实际上就是定义.py,其中可以包含:变量定义,可执行语句,for循环,函数定义等等,他们统称模块的成员。

    模块的运行方式:

    • 脚本方式:直接用解释器执行。或者PyCharm中右键运行。
    • 模块方式:被其他的模块导入。为导入它的模块提供资源(变量,函数定义,类定义等)。

    __name__属性的使用:

    在脚本方式运行时,__name__是固定的字符串:__main__

    在以模块方式被导入时,__name__就是本模块的名字。

    在自定义模块中对__name__进行判断,决定是否执行可执行语句:开发阶段,就执行,使用阶段就不执行。

    系统导入模块的路径

    • 内存中:如果之前成功导入过某个模块,直接使用已经存在的模块
    • 内置路径中:安装路径下:Lib
    • PYTHONPATH:import时寻找模块的路径。
    • sys.path:是一个路径的列表。

    如果上面都找不到,就报错。

    通过动态修改sys.path的方式将自定义模块添加到sys.path中。

    os.path.dirname():获取某个路径的父路径。通常用于获取当前模块的相对路径

    import sys
    import os
    sys.path.append(os.path.dirname(__file__) + '/aa')

    导入模块的多种方式:

    • import xxx:导入一个模块的所有成员
    • import aaa,bbb:一次性导入多个模块的成员。不推荐这种写法,分开写。
    • from xxx import a:从某个模块中导入指定的成员。
    • from xxx import a,b,c:从某个模块中导入多个成员。
    • from xxx import *:从模块中导入所有成员。

    import xxx 和 from xxx import * 的区别

    第一种方式在使用其中成员时,必须使用模块名作为前缀。不容易产生命名冲突。

    第二种方式在使用其中成员时,不用使用模块名作为前缀,直接使用成员名即可。但是容易产生命名冲突。在后定义的成员生效(把前面的覆盖了。)

    怎么解决名称冲突的问题

    • 改用import xxx这种方式导入。
    • 自己避免使用同名
    • 使用别名解决冲突

    使用别名:alias

    给成员起别名,避免名称冲突。

    from my_module import age as a

    给模块起别名,目的简化书写。

    import my_module as m

    from xxx import * 控制成员被导入

    默认情况下,所有的成员都会被导入。

    __all__是一个列表,用于表示本模块可以被外界使用的成员。元素是成员名的字符串。

    注意:

    __all__只是对from xxx import *这种导入方式生效。其余的方式都不生效。

    相对导入

    针对某个项目中的不同模块之间进行导入,称为相对导入。

    只有一种格式:

    from 相对路径 import xxx

    相对路径:包含了点号的一个相对路径。

    . 表示的是当前的路径。

    ..表示的是父路径。

    ...表示的是父路径的父路径。

    # 相对导入同项目下的模块
    # from ..z import zz            # 容易向外界暴露zz模块
    from ..z.zz import *
    # 不使用相对导入的方式,导入本项目中的模块
    # 通过当前文件的路径找到z的路径
    import os
    import sys
    sys.path.append(os.path.dirname(os.path.dirname(__file__)) + '/z')
    from zz import *

    常用模块:random

    此模块提供了和随机数获取相关的方法:

    • random.random():获取[0.0,1.0)范围内的浮点数
    • random.randint(a,b):获取[a,b]范围内的一个整数
    • random.uniform(a,b):获取[a,b)范围内的浮点数
    • random.shuffle(x):把参数指定的数据中的元素打乱。参数必须是一个可变的数据类型。
    • random.sample(x,k):从x中随机抽取k个数据,组成一个列表返回。
    1. 今日总结

    2. 预习内容

    转载于:https://www.cnblogs.com/miseryGOD/p/10883089.html

    展开全文
  • 讲义整理-线程基础、线程之间共享和协作线程基础、线程之间共享和协作什么是进程和线程CPU核心数和线程数关系CPU时间片轮转机制澄清并行和并发高并发编程意义、好处和注意事项多线程程序需要注意事项 ...
  • 线程基础以及线程之间共享与协作基础概念什么是进程和线程CPU核心数和线程数关系CPU时间片轮转机制理解并行和并发高并发编程意义和好处多线程程序注意事项认识Java里线程Java程序天生就是多线程线程启动...
  • java中创建线程方式

    2020-03-23 00:25:19
    进程操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘 IO 等。同一进程中多条线程共享该进程中全部系统资源。而一个进程就是由一个或者多个线程组成。线程就是 CPU 调度的最小单位。 多...
  • 1、什么是进程和线程1.1 进程是程序运行资源分配的最小单位进程是操作系统进行资源分配的最小单位其中资源包括:CPU、内存空间、磁盘IO等,同一进程中多个线程共享该进程中全部系统资源,而进程和进程之间是...
  • 认识Java中线程

    千次阅读 2018-08-18 18:22:51
    前言:最近在看Java中并发,做了笔记,但是还是觉得记录一下比较好。加深理解,这个模块可能有很多篇...进程操作系统进行资源分配的最小单位其中资源包括:CPU、内存空间、磁盘IO等,同一进程中多条线程共...
  • 1. 计算机字长一般指的是,所谓n位CPU其中n什么? 在同一时间中处理二进制数位数叫字长。通常称处理字长为8位数据CPU叫8位CPU,32位CPU就是在同一时间内处理字长为32位二进制数据。 2.计算机字长...
  • 线程被称为轻量级进程,程序执行的最小单位,它指在程序执行过程中,能够执行代码一个执行单位。每个程序程序都至少有一个线程,也即程序本身。2.线程状态Java语言定义了5种线程状态,在任意一个时间点,一...
  • 创建多线程4种方式

    2018-10-18 17:19:00
    线程被称为轻量级进程,程序执行的最小单位,它指在程序执行过程中,能够执行代码一个执行单位。每个程序程序都至少有一个线程,也即程序本身。 2.线程状态 Java语言定义了5种线程状态,在任意一个时间...
  • 线程被称为轻量级进程,程序执行的最小单位,它指在程序执行过程中,能够执行代码一个执行单位。每个程序程序都至少有一个线程,也即程序本身。 2.线程状态 Java语言定义了5种线程状态,在任意一个时间点...
  • 网络编程(一)

    2018-05-22 14:40:56
    1.什么是进程,什么是线程? 进程与线程都是一个时间描述,是CPU工作时间段... 进程是资源分配的最小单位,线程是CPU执行的最小单元。 2.什么时候调用进程,什么时候调用线程? | 执行情况 | 调用者 | ...
  • 同一个时间段内,不同任务模块可以并发进行,称为线程,CPU的最小调度单位。 进程和线程区别: 从资源分配角度: 进程资源分配的最小单位,一个进程多个线程共享进程资源。 从cpu调度角度: 所有...
  • 数制和码制概述

    千次阅读 2016-03-11 23:02:01
    其中一类物理量变化在时间上和数量上都离散,也就是说,它们变化在时间不连续,总是发生在一系列离散瞬间。而且,它们数值大小和每次增减变化都某一个最小数量单位的整数倍,而小于这个最小...
  • LINGO软件学习

    2009-08-08 22:36:50
    2.2 什么是集 集是一群相联系对象,这些对象也称为集成员。一个集可能是一系列产品、卡车或雇员。每个集成员可能有一个或多个与之有关联特征,我们把这些特征称为属性。属性值可以预先给定,也可以是未知,...
  • 超级有影响力的Java面试题大全文档 1.抽象: 抽象就是忽略一个主题中与当前目标无关...EntityBean:Entity Beans能存活相对较长的时间,并且状态持续的。只要数据库中的数据存在,Entity beans就一直存活。而不是...
  • Linux多线程编程

    2016-10-29 23:55:13
    什么是进程? 进程是一个具有一定独立功能程序一次运行活动,同时也是资源分配的最小单元;从内核观点看,进程目的就是担当分配系统资源(CPU时间、内存等)基本单位。 Linux系统是一个多进程系统,它...
  • 这个DirtyUnpack的Sight,一个从网络访问的动态Sight(snapshot类型的太大了,­要400GB,软盘没那么大空间),这种动态的sight速度不够快的,如果在较高层次的目录上进行查找,往往需要1/356光年的时间才能完成...
  • 24MHz,但是单片机对时间的要求比较高,能够精确定时一秒,所以也为了 方便计算我们选择12MHz 晶振。 晶振两边电容:晶振标称值在测试时有一个“负载电容”条件,在工 作时满足这个条件,振荡频率才与...
  • 线程操作系统能够进行运算调度的最小单位。它被包含在进程之中,进程中实际运作单位 进程执行逻辑 通过CPU切换时间切片,执行相应进程 2.为什么要有线程 1.效率方面分析 同一进程中,如果需要执行多个任务...
  • 谈论x1(t)的时间采样还是谈论在给定时间x1(t)的所有可能版本? 174 3.3.6 如何测量非高斯性? 177 3.3.7 如何计算一个随机变量的矩? 178 3.3.8 峰度如何定义的? 178 3.3.9 负熵如何定义的? 180 3.3.10...
  • java常用工具类使用

    热门讨论 2012-03-19 20:11:37
    格式化的目的把一个对象以不同的格式表示,以满足不同环境对格式的要求,比如:前面学习的Date对象实质一个以毫秒值表示的时间,但是在不同的国家和地区表示方式不一样。那么就需要对Date进行格式化处理。接下来...
  • sentinel降级规则

    千次阅读 2020-09-09 16:38:33
    慢调用比例:当资源响应时间超过最大RT(以ms为单位,最大RT即最大响应时间)之后,资源进入准降级状态。如果接下来1s内持续进入5个请求(最小请求数),它们RT都持续超过这个阈值,那么在接下来熔断时长之内...

空空如也

空空如也

1 2 3 4 5
收藏数 84
精华内容 33
关键字:

其中什么是最小的时间单位