java并发编程汇总1:并发、java线程(包括:状态、阻塞唤醒等)

在此将已掌握的并发编程知识进行汇总,形成一个框架、体系,便于以后的查阅。

一、并发的概念

1.1、什么是并发;

  • 通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升;
  • 面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。

1.2、进程与线程;

  • 进程:分配和管理资源的基本单位
  • 线程:CPU调度的最小单位,必须依赖进程而存在。
  • 对Java语言来说,Java程序是运行在JVM上面的,每一个JVM其实就是一个进程。所有的资源分配都是基于JVM进程来的。而在这个JVM进程中,又可以创建出很多线程,多个线程之间共享JVM资源,并且多个线程可以并发执行。

1.3、java线程调度策略:抢占式(java采用)、协同式;

1.4、多线程编程的优点、缺点(上下文切换、死锁(线程安全)等);

  • 上下文切换(由于此,并发不一定比串行快):当并发执行累加操作在千万级以下并发会比串行要慢,当千万级以上并发要比串行要快,这正是因为线程有创建和上下文切换开销。

二、java线程

2.1、java线程:

  • 每个程序都至少拥有一个线程-即作为Java虚拟机(JVM)启动参数运行在主类main方法的线程。
  • 一个java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上java程序天生就是一个多线程程序,包含了:(1)分发处理发送给给JVM信号的线程;(2)调用对象的finalize方法的线程;(3)清除Reference的线程;(4)main线程,用户程序的入口。

2.2、java线程的三种创建方式:

  • 继承Thread类;
  • 实现Runnable接口;
  • 实现Callable接口;

具体demo见:

 public class CreateThreadDemo {
 
     public static void main(String[] args) {
         //1.继承Thread
         Thread thread = new Thread() {
             @Override
             public void run() {
                 System.out.println("继承Thread");
                 super.run();
             }
         };
         thread.start();


         //2.实现runable接口
         Thread thread1 = new Thread(new Runnable() {
             @Override
             public void run() {
                 System.out.println("实现runable接口");
             }
         });
         thread1.start();


         //3.实现callable接口
         ExecutorService service = Executors.newSingleThreadExecutor();
         Future<String> future = service.submit(new Callable() {
             @Override
             public String call() throws Exception {
                 return "通过实现Callable接口";
             }
         });
         try {
             String result = future.get();
             System.out.println(result);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } catch (ExecutionException e) {
             e.printStackTrace();
         }
     }
 
 }
  • 由于java不能多继承、但可以实现多个接口,因此,在创建线程的时候尽量多考虑采用实现接口的形式;
  • 实现Callable接口,提交给ExecutorService返回的是异步执行的结果;
  • 另外,通常也可以利用FutureTask(Callable<V> callable)将callable进行包装然后FeatureTask提交给ExecutorsService。具体见下:
FutureTask<String> futureTask = new FutureTask<>(new Callable() {
     @Override
     public String call() throws Exception {
         return "通过实现Callable接口";
     }
 });
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
  • FutureTask接口的实现关系:

2.3、Java线程的优先级:

  • 实际上,JVM的线程调度器的调度策略决定了上层多线程的运行机制。JVM的线程调度器使用了抢占式调度方式,每条线程执行的时间都由它分配管理。它将按照线程优先级的建议对线程的执行时间进行分配,优先级越高,可能得到CPU的时间则越长。
  • Java 把线程优先级分成10个级别,线程被创建时如果没有明确声明则使用默认优先级。JVM将根据每个线程的优先级来分配执行时间的概率。Java定义了三个常量:
public final static int MIN_PRIORITY = 1; //最小优先级。
public final static int NORM_PRIORITY = 5; //默认优先级
public final static int MAX_PRIORITY = 10; //最大优先级
  • 此外,由于JVM的实现以宿主操作系统为基础,所以Java优先级值与各种不同操作系统的原生线程优先级必然存在着某种映射关系,这样才能够封装所有操作系统的优先级提供统一优先级语义。例如1-10优先级值在Linux可能要与-20-19优先级值进行映射,而Windows系统则有9个优先级要映射。

2.4、java线程的状态及转换:

此图来源于《JAVA并发编程的艺术》一书中,线程是会在不同的状态间进行转换的,java线程线程转换图如上图所示。

  • 线程创建之后调用start()方法开始运行;
  • 当调用wait()、join()、LockSupport.lock()方法线程会进入到WAITING状态;
  • 而同样的wait(long timeout)、sleep(long)、join(long)、LockSupport.parkNanos()、LockSupport.parkUtil()增加了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING状态,当超时等待时间到达后,线程会切换到Runable的状态,另外当WAITING和TIMED _WAITING状态时可以通过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态;
  • 当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED阻塞状态,当线程获取锁时,线程进入到Runable状态。线程运行结束后,线程进入到TERMINATED状态,状态转换可以说是线程的生命周期。另外需要注意的是:
  • synchronized与lock的区别:当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。

六种状态说明:

2.5、java线程的终中断机制:

有时我们希望在任务执行中取消该任务,使线程停止,这时就会涉及到中断操作。在Java中要让线程安全、快速、可靠地停下来并不是一件容易的事,Java也没有提供任何可靠的方法终止线程的执行。

线程调度策略中有抢占式和协作式两种模式,中断机制也类似,对应着抢占模式和协作模式。抢占式中断就是直接向某个线程发起中断指令,此时不管线程处于什么状态都必须立即中断。协作式中断的核心是维护一个中断标识,当标识被标记成中断后,线程在适当的时间节点执行中断操作。

  • 抢占式中断:thread.stop(),现JDK已弃用;
  • 协作式中断:现在采用的方式;

经历了很长时间的发展,Java最终选择用一种协作式的中断机制实现中断;

协作式中断的原理很简单,其核心使用一个中断变量作为标识,即对某线程的中断标进行识位,被标记了中断位的线程在适当的时间节点会抛出异常,我们捕获异常后做相应的处理。

jdk为我们提供了三个函数来中断线程:

  • interrupt():Thread实例方法,必须由其它线程获取被调用线程的实例后,进行调用。实际上,只是改变了被调用线程的内部中断状态
  • Thread.interrupted():Thread类方法,必须在当前执行线程内调用,该方法返回当前线程的内部中断状态,然后清除中断状态(置为false) 。
  • isInterrupted():Thread实例方法:用来检查指定线程的中断状态。当线程为中断状态时,会返回true;否则返回false。换句话说:当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。

  • 中断机制可作为线程间简单交互的一种方式:

同样可以通过中断的方式实现线程间的简单交互, while (sleepThread.isInterrupted()) 表示在Main中会持续监测sleepThread,一旦sleepThread的中断标志位清零,即sleepThread.isInterrupted()返回为false时才会继续Main线程才会继续往下执行。因此,中断操作可以看做线程间一种简便的交互方式。一般在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。

2.6、join

join方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出,这就像两个好基友,一个基友先走在前面突然看见另一个基友落在后面了,这个时候他就会在原处等一等这个基友,等基友赶上来后,就两人携手并进;

其实线程间的这种协作方式也符合现实生活。在软件开发的过程中,从客户那里获取需求后,需要经过需求分析师进行需求分解后,这个时候产品,开发才会继续跟进;

如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行;

关于join方法一共提供如下这些方法:

public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos)
public final void join() throws InterruptedException

Thread类除了提供join()方法外,另外还提供了超时等待的方法,如果线程threadB在等待的时间内还没有结束的话,threadA会在超时之后继续执行。join方法源码关键是:

 while (isAlive()) {
    wait(0);
 }

可以看出来当前等待对象threadA会一直阻塞,直到被等待对象threadB结束后即isAlive()返回false的时候才会结束while循环,当threadB退出时会调用notifyAll()方法通知所有的等待线程。

2.7、守护线程:

Java分为两种线程:用户线程守护线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作;

这意味着,当一个Java虚拟机中不存在非Daemon线程的时候(不存在用户线程),Java虚拟机将会退出;

设置守护线程的方式:可以通过调 用Thread.setDaemon(true)将线程设置为Daemon线程。

注意:Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块 并不一定会执行。所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的。

2.8、yield:

public static native void yield(),这是一个静态方法,一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行;

另外,让出的时间片只会分配给当前线程相同优先级的线程。

sleep和yield的不同:

  • sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片;
  • 而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。

2.9、wait。notify、notifyall:

关于EntrySetWaitSet还是看自己写的博客吧:

wait()/notify()的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类 java.lang.Object上。

经典案例:wait、notify实现生产者-消费者:

public class MyTest {
    /**
     * 仓库
     */
    private static class Storage {
        //仓库容量
        private static final int FULL_COUNT = 10;
        private LinkedList<Object> store = new LinkedList<>();

        public synchronized void push(Object object) {
            while (store.size() == FULL_COUNT) {
                System.out.println("仓库容量已满,请等待!");
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            store.add(object);
            System.out.println("产品入库 ThreadName:" + Thread.currentThread().getName() + " size:" + store.size());
            //通知所有的消费者
            notifyAll();
        }

        public synchronized void getOut() {
            while (store.size() == 0) {
                System.out.println("仓库已空,请等待!ThreadName:" + Thread.currentThread().getName() + " size:" + store.size());
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            store.remove(0);
            System.out.println("产品出库 ThreadName:" + Thread.currentThread().getName() + " size:" + store.size());
            //通知所有的生产者
            notifyAll();
        }
    }

    /**
     * 生产者
     */
    private static class Produce extends Thread {
        private Storage storage;

        public Produce(String name, Storage storage) {
            super(name);
            this.storage = storage;
        }

        @Override
        public void run() {
            while (true) {
                storage.push(new Object());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 生产者
     */
    private static class Consumer extends Thread {
        private Storage storage;

        public Consumer(String name, Storage storage) {
            super(name);
            this.storage = storage;
        }

        @Override
        public void run() {
            while (true) {
                storage.getOut();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Storage storage = new Storage();
        //三个生产者
        for (int i = 0; i < 3; i++) {
            new Produce("Produce" + i, storage).start();
        }
        //三个消费者
        for (int i = 0; i < 3; i++) {
            new Consumer("Consumer" + i, storage).start();
        }
    }
}

2.10、sleep:

public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器;

需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。sleep方法经常拿来与Object.wait()方法进行比价,这也是面试经常被问的地方。

sleep、wait两者主要的区别:

  1. sleep()方法是Thread的静态方法,而wait是Object实例方法
  2. wait()方法必须要在同步方法或者同步块中调用也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
  3. sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。

2.11、park、unpark:

LockSupport类为线程阻塞唤醒提供了基础,同时,在竞争条件问题上具有wait和notify无可比拟的优势;

使用wait和notify组合时,某一线程在被另一线程notify之前必须要保证此线程已经执行到wait等待点,错过notify则可能永远都在等待,另外notify也不能保证唤醒指定的某线程。反观LockSupport,由于park与unpark引入了许可机制许可逻辑为:初始时,permit为0,调用pack时permit等于0时阻塞,permit等于1时返回并将permit置0。调用unpark时,尝试唤醒线程,permit加1。

对于同一条线程,park与unpark先后操作的顺序似乎并不影响程序正确地执行。假如先执行unpark操作,许可则为1,之后再执行park操作,此时因为许可等于1直接返回往下执行,并不执行阻塞操作。

wait/notify、park/unpark的区别:或者说park/unpark的优势

  • LockSupport的park与unpark组合真正解耦了线程之间的同步,不再需要另外的对象变量存储状态,并且也不需要考虑同步锁,wait与notify要保证必须有锁才能执行,而且执行notify操作释放锁后还要将当前线程扔进该对象锁的等待队列,LockSupport则完全不用考虑对象、锁、等待队列等问题。

实际上LockSupport阻塞和唤醒线程的功能是依赖于sun.misc.Unsafe,这是一个很底层的类,有兴趣的可以去查阅资料,比如park()方法的功能实现则是靠UNSAFE.park(false, 0L),unpark()方法是 UNSAFE.unpark(thread)实现的;

注意:

  • synchronzed致使线程阻塞,线程会进入到BLOCKED状态,而调用LockSupprt方法阻塞线程会致使线程进入到WAITING状态。

总结:

  • 线程阻塞唤醒三贱客:sleep、wait/notify、park/unpark;

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值