java高并发基础篇之Java与线程

前言

并发不一定要依赖多线程(如PHP中很常见的多进程并发),但是在Java里面谈论 并发,大多数都与线程脱不开关系。

一、线程的实现

我们知道,线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等), 又可以独立调度(线程是CPU调度的最基本单位)。

主流的操作系统都提供了线程实现,Java语言则提供了在不同硬件和操作系统平 台下对线程操作的统一处理,每个java.lang.Thread类的实例就代表了一个线程。不过 Thread类与大部分的Java API有着显著的差别,它的所有关键方法都被声明为Native. 在Java API中一个Native方法可能就意味着这个方法没有使用或无法使用平台无关的手段来实现(当然也可能是为了执行效率而使用Native方法,通常最高效率的手段也就是平台相关的手段)。

1、内核线程

内核线程(Kernel Thread, KLT)就是直接由操作系统内核(Kernel,下称内核) 支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程都可以看做是内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫多线 程内核(Multi-Threads Kernel)。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process, LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型,如 图12-3所示。
在这里插入图片描述

由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作,但是轻量级进程具有它 的局限性:首先,由于是基于内核线程实现的,所以各种进程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode) 和内核态(Kernel Mode)中来回切换。其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。

2.使用用户线程实现

广义上来讲,一个线程只要不是内核线程,那就可以认为是用户线程(User Thread, UT),因此从这个定义上来讲轻量级进程也属于用户线程,但轻量级进程的实 现始终是建立在内核之上的,许多操作都要进行系统调用,因此效率会受到限制。

而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型,如图 12-4所示。
在这里插入图片描述
使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援, 所有的线程操作都需要用户程序自己处理。线程的创建、切换和调度都是需要考虑问题,而且由于操作系统只把处理器资源分配到进程,那诸如"阻塞如何处理”、’'多处 理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至不可能完成。因而使用用户线程实现的程序一般都比较复杂气除了以前在不支持多线 程的操作系统中(如DOS)的多线程程序与少数有特殊需求的程序外,现在使用用户线程的程序越来越少了,Java、Ruby等语言都曾经使用过用户线程,最终又都放弃 使用它。

3.混合实现

线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式。在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则 作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器 映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了进程被阻塞的风 险。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,是M:N的关系, 如图12-5所示,这种就是多对多的线程模型。
许多Unix系列的操作系统,如Solaris, HP-UX等都提供了 M:N的线程模型实现。
在这里插入图片描述

4.Java线程的实现

Java线程在JDK 1.2之前,是基于名为“绿色线程”(Green Threads)的用户线程实现的,而在JDK 1.2中,线程模型被替换为基于操作系统原生线程模型来实现。因此 在目前的JDK版本中,操作系统支持怎样的线程模型,在很大程度上就决定了 Java虚拟机的线程是怎样映射的,这点在不同的平台上没有办法达成一致,虚拟机规范中也并未限定Java线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成 本产生影响,对Java程序的编码和运行过程来说,这些差异都是透明的。

对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型来实现 的,一条Java线程就映射到一条轻量级进程之中,因为Windows和Linux系统提供的线程模型就是一对一的,

而在Solaris平台中,由于操作系统的线程特性可以同时支持一对一(通过 Bound Threads 或 Alternate Libthread 实现)及 多对多(通过 LWP / Thread Based Synchronization实现)的线程模型,因此在Solaris版的JDK中也对应提供了两个平台 专有的虚拟机参数:-XX:+UseLWPSynchronization (默认值)和-XX:+UseBoundThreads 来明确指定虚拟机使用的是哪种线程模型。

二、Java线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度抢占式线程调度

1、协同式线程调度

如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去。协同式多线程 的最大好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。

Lua语言中的“协同例 程”就是这类实现。它的坏处也很明显:线程执行时间不可控制,甚至如果一个线程编 写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。很久以前的 Windows 3.x系统就是使用协同式来实现多进程多任务的,那是相当的不稳定,一个进 程坚持不让出CPU执行时间就会导致整个系统的崩潰。

2、抢占式线程调度

如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。在这种实现线程调度的方式下,线程 的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java使用的线 程调度方式就是抢占式调度七 与前面所说的Windows 3.x的例子相对,在Windows 9x/ NT内核中就是使用抢占式来实现多进程的,当一个进程出了问题,我们还可以使用任 务管理器把这个进程杀掉,而不至于导致系统崩溃。

三、Java线程创建的方式

1、继承Thread类并重载run方法
Thread(
       ThreadGroup group,//可以为空,如果没有设置,那么其线程组就是其父线程的线程组,此时子线程和父线程属于同一个线程组
       Runnable target, //可以为空,启动此线程时调用其run方法的对象。 如果null ,则调用此线程的run方法
       String name, //可以为空,线程的名字,如果不显示指定,默认线程名字是:Thread-0(这个数字是递增的)
       long stackSize //可以为空,新线程所需的堆栈大小,或为零表示此参数将被忽略,注意有些平台设置这个参数是无效的
)

Thread类:是专门用来创建线程和对线程进行 操作的类。Thread中定义了许多方法对线程进 行操作。
Thread类在缺省情况下run方法什么都不做。 可以通过继承Thread类并重写Thread类的run 方法实现用户线程。

Thread的静态方法currentThread()可以返回当前线程引用
Thread的getThreadGroup()返回此线程所属的线程组

class Thread1 extends Thread{
    public Thread1(String name){super(name);}
    public void run(){
        for(int i = 0; i < 100; i++){
            System.out.println("hello world: " + i);
        }
    }
}
class Thread2 extends Thread{
    public Thread2(String name){super(name);}
    public void run(){
        for(int i = 0; i < 100; i++) {
            System.out.println("welcome: " + i);
        }
    }
}
public class ThreadTest{
    public static void main(String[] args) {
        Thread1 t1 = new Thread1("first thread");
        Thread2 t2 = new Thread2("second thread");
       
        System.out.println(t1.getName());
        System.out.println(t2.getName());
       
        t1.start();
        t2.start();

    }
}

交替运行
2、实现Runnable接口的类实现run方法。

实现Runnable接口相对于继承Thread类的优点

1、适合多个相同程序代码的线程去处理同一个资源
2、可以避免由于Java的单继承性带来的局限性
3、增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。

class MyThread implements Runnable{
   public void run(){
    for(int i = 0; i < 100; i++){
        System.out.println(Thread.currentThread().getName()+":"+ i);
    }
}
public class Demo {
	public static void main(String[] args){
		MyThread myThread =new MyThread();
	    Thread t1 = new Thread(myThread,"A");
        Thread t2 = new Thread(myThread,"B");
        t1.start();
        t2.start();		
	}
}
3、实现Callable接口

Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。注意Callable 接口与Thread没有关系

public class LifTOff implements Callable<String> {
      private int number = 10;
      private static int count = 0;
      private final int id = count++;
      @Override
      public String call() throws Exception {
            for(int i = 0;i<8;i++){
            System.out.println(Thread.currentThread().getName()+"  number:"+number--);
            }
            return Thread.currentThread().getName()+"线程执行完毕";
      }
            
}


public class LifTOffTest {
      public static void main(String[] args) throws InterruptedException, ExecutionException {
            FutureTask<String> f = new FutureTask<String>(new LifTOff());
            FutureTask<String> f1 = new FutureTask<String>(new LifTOff());
            new Thread(f).start();
            new Thread(f1).start();
            
      }
}
Thread-1  number:10
Thread-0  number:10
Thread-1  number:9

三、状态转换

Java语言定义了6种进程状态,在任意一个时间点中,一个进程只能有且只有其中 的一种状态,

6种状态在遇到特定事件发生的时候将会互相转换,它们的转换关系如图下所示。
在这里插入图片描述

1、状态说明

1、新建(New):创建后尚未启动的线程处于这种状态。

2、运行(Runable): Runable包括了操作系统线程状态中的Running和Ready,也 就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执 行时间。

相关Thread方法如下:

void    start()//线程进入runnable状态
static void yield()
//线程让步。使用了这个方法之后,线程会让出CPU执行权,让自己或者其它的线程运行,就是说下一次运行有可能是自己或者其他线程

3、无限期等待(Waiting):处于这种状态的进程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。

以下方法会让线程陷入无限期的等待状态:

//没有设置Timeout参数的
Object.wait()
Thread.join()//必须在线程start后执行,否则无效,比如在线程B中调用了线程A.join,那么B会等到A执行完成后再执行
LockSupport.park()

4、限期等待(Timed Waitting):处于这种状态的进程也不会被分配CPU执行时间, 不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。

以下方法会让线程进入限期等待状态:

这是一个静态方法,线程睡眠,没有放弃线程锁,只是放弃cpu执行权
TimeUnit.SECONDS.sleep(long d)
Thread.sleep(long millis, int nanos)

object.wait(long timeout)
object.wait(long timeout, int nanos)

必须在线程start后执行,否则无效,比如在线程B中调用了线程A.join,那么B会等到A执行完成后再执行
thread.join(long millis)
thread.join(long millis, int nanos)

LockSupport.parkNanos()方法。
LockSupport.parkUntil()方法。

面试热点:wait和sleep区别
共同点:
他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
不同点:
sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。
wait()使用时,其wait()的对象必须使用synchronized,否则报错。而 sleep()不用

5、阻塞(Blocked):进程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞 状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时 候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等 待进入同步区域的时候,线程将进入这种状态。

6、结束(Terminated):已终止线程的线程状态,线程已经结束执行。目前,线程有三种方法可以结束:

为什么弃用stop:
调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题

1.设置退出标志,使线程正常退出,也就是当run()方法完成后线程终止

public class VolatileDemo1 {
    //线程通过sign来控制开关
    private  static volatile boolean sign = false;
    private static void stop(){
        while (true){
            if (sign){
                System.out.println(Thread.currentThread().getName()+"我结束了");
                break;
            }
        }
    }
    public static void main(String[] args) {
        for(int i = 0;i<30;i++){
            new Thread(VolatileDemo1::stop).start();
        }
        SleepUtil.sleep(1);
        //在main线程中把线程开关标志变为true,欲关闭其他线程
        sign =true;
        System.out.println(sign);
    }
}

2.使用interrupt()方法中断线程,相关方法如下

void  interrupt()//中断这个线程。注意次方法只会中断抛出InterruptedException方法的线程

boolean  isAlive()//测试这个线程是否活着。

boolean  isInterrupted()//测试这个线程是否被中断。

static boolean  interrupted()//测试当前线程是否中断。

案例

public class StopMethodOfThread2 {
    /**
     * 注意:如果没有break,那么线程调用interrupt方法不会关闭线程,因为异常InterruptedException被捕获了
     * while循环不会停止,所以如果不使用break,可以把整个while循环放在try 里
     */
    public void start(){
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("线程正在关闭中.....");
                break;
            }
            System.out.println("线程正在运行中.....");
        }
        System.out.println("线程已经关闭");
    }
    public static void main(String[] args) throws InterruptedException {
        StopMethodOfThread2 stopMethodOfThread2 = new StopMethodOfThread2();
        Thread thread =  new Thread(()->stopMethodOfThread2.start());
       
        thread.start();
        System.out.println(thread.isInterrupted());//false
        thread.interrupt();
        System.out.println(thread.isInterrupted());//true
    }
}

这里在额外说明一下使用interrupt()打断使用join()线程的方法

案例1
在这段代码中,我们本意1秒后让main线程结束。thread1不管

Thread thread1 = new Thread(()->{  while(true){}  });

Thread thread2 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("开始打断thread1");
            thread1.interrupt();
});

thread1.start();
thread2.start();
        
try { 
   thread1.join();
 } catch (InterruptedException e) {
   e.printStackTrace();
}
System.out.println("main线程结束了");

实际效果是:代码进入catch块不了。interrupt方法好像是无效的,这是为什么呢?
原因是:thread1.join();这段代码是在main线程中,他是将main线程join住了。所以代码应该如下

Thread thread1 = new Thread(()->{  while(true){}  });
Thread main = Thread.currentThread();
Thread thread2 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("开始打断thread1");
            main.interrupt();
});

thread1.start();
thread2.start();
        
try { 
   thread1.join();
 } catch (InterruptedException e) {
   e.printStackTrace();
}
System.out.println("main线程结束了");

3.使用stop方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)

前两种方法都可以实现线程的正常退出;第3种方法相当于电脑断电关机一样,是不安全的方法。

2、java获取线程状态

我们可以利用下面的方法获取当前线程的状态

Thread.State    getState()

Thread.State值如下
BLOCKED:(blocked)一个线程的线程状态阻塞等待监视器锁定。            
NEW:(new)线程尚未启动的线程状态。            
RUNNABLE:(runnable)可运行线程的线程状态。           
TERMINATED(terminated)终止线程的线程状态。          
TIMED_WAITING(timed_waiting)具有指定等待时间的等待线程的线程状态。        
WAITING(waiting)等待线程的线程状

四、守护线程与用户线程

java提供了两种线程:守护线程与用户线程。

守护线程:又被称为服务进程、精灵线程、后台线程,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的 “保姆”。

利用java中设置守护线程,代码如下:

//注意setDaemon必须写在start前面,否则线程启动后在设置,就无效了
thread.setDaemon(true)//如果是false,当前线程就是用户线程
thread.start()

如果我们想判断当前线程是否是守护线程,可以使用Thread的isDaemon方法

//测试该线程是否为守护线程。true是守护线程
boolean isDaemon() 

用户线程:又被称为前台线程,是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。

用户线程和守护线程几乎一样,唯一的不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM也就退出了。因为当所有非守护线程结束时,没有了被守护者, 守护线程没有工作可做,也就没有继续运行程序的必要了,程序也就终止了,同时会 “杀死”所有守护线程。也就是说,只要有任何非守护线程还在运行,程序就不会终止。

我们所熟悉的Java垃圾回收线程就是一个典型的守护线程,当我们的程序中不再有任何运行中的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值