java 线程

实现方式

Thread
  1. 继承Thread类,重写run方法
  2. 创建子类实例,即创建了线程对象
  3. 调用实例的start方法来启动线程
public class MyThread extends Thread {
    // 多个线程无法共享线程类的实例变量
    private int i;
    
    @override
    public void run() {
        // 线程需要完成的任务
        // run()方法是一个普通方法
        ...
    }
}

public static void main(String[] args) {
	MyThread t = new MyThread();
    // 首先启动了线程,然后由jvm去调用该线程的run()方法
    t.start();
}
Runnable
  1. 实现Runnable接口,实现run方法
  2. 创建类实例,将此实例作为Thread的target创建一个Thread对象,该Thread对象才是线程对象
  3. 调用线程对象的start方法启动该线程
public class MyThread implements Runnable {
    // 创建多个线程可以共享线程类的实例变量
    // 因为程序创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享一个实例变量
    private int i;
    
    @override
    public void run() {
        // 线程需要完成的任务
        ...
    }
}

public static void main(String[] args) {
	MyThread mt = new MyThread();
    Thread t = new Thread(mt, "myThread");
    t.start();
}
Callable

看起来和Runnable很像,但call方法可以有返回值、可以抛出异常。

  1. 实现Callable接口,实现call方法
  2. 使用FutureTask类来包装Callable对象,该FutureTask封装类Callable的call方法的返回值
  3. 使用FutureTask对象作为Thread的target创建并启动新线程
  4. 使用FutureTask的get方法获取执行结束后的返回值
public class MyThread implements Callable<Integer> {
    private int i;
    
    @override
    public Integer call() throws Exception {
        // 线程执行体,具有返回值
        ...
    }
}

public static void main(String[] args) {
	MyThread mt = new MyThread();
    FutureTask<Integer> ft = new FutureTask<>(mt);
    Thread t = new Thread(ft, "myThread");
    t.start();
    
    // 获取执行结束后的返回值
    try {
        ft.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}
future

Java5提供了Future接口来代表Callable接口的call方法的返回值,并为Future接口提供了一个FutureTask实现类,FutureTask实现了Future接口和Runnable接口——可以作为Thread的target。

Future提供了三种功能:判断任务是否完成;能够中断任务;能够获取任务执行结果。

底层实现异步原理:在客户端请求的时候,直接返回客户端需要的数据(此数据不一定完整,只是简单的一点不耗时的操作),但是客户端并不一定马上使用所有的信息,此时就有了时间去完善客户需要的信息

与FutureTask的区别和联系:future是个接口,futuretask可以通过实现该接口,调用get方法返回执行结果

总结

Runnable、Callable的优势在于——线程类只是实现了Runnable或Callable接口,还可以继承其它类;在这种方法下,多个线程可以共享一个target对象,因此非常适合多个相同线程处理同一份资源的情况,从而将CPU、代码和数据分开,形参清晰的模型,体现了面对对象的编程思想。

劣势在于编程复杂度略高。

守护线程

在Java中有两类线程:User Thread用户线程、Daemon Thread守护线程

只要当前JVM实例中尚存在一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC。当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

不是所有的任务都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑就不可以,因为一旦User退出了,可能大量数据还没有来得及读入或写出。

守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。

Thread daemonThread = new Thread();
// 设定 daemonThread 为守护线程,default false(非守护线程)
// 必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。
daemonThread.setDaemon(true);
// 验证当前线程是否为守护线程,返回 true 则为守护线程
System.out.println(daemonThread.isDaemon());

在Daemon线程中产生的新线程也是Daemon的。

RE判断程序执行结束的标准是所有的非守护执线程行完毕,而不管守护线程的状态。

线程则是JVM级别的,如果你在Web 应用中启动一个线程,这个线程的生命周期并不会和Web应用程序保持同步。也就是说,即使你停止了Web应用,这个线程依旧是活跃的。正是因为这个很隐晦的问题,所以很多有经验的开发者不太赞成在Web应用中私自启动线程。

Spring为JDK Timer和Quartz Scheduler所提供的TimerFactoryBean和SchedulerFactoryBean能够和Spring容器的生命周期关联,在 Spring容器启动时启动调度器,而在Spring容器关闭时,停止调度器。所以在Spring中通过这两个FactoryBean配置调度器,再从 Spring IoC中获取调度器引用进行任务调度将不会出现这种Web容器关闭而任务依然运行的问题。而如果你在程序中直接使用Timer或Scheduler,如不 进行额外的处理,将会出现这一问题。

多线程上下文切换

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换

减少上下文切换
  • 让步式上下文切换:执行线程主动释放CPU,与锁竞争严重程度成正比,可通过减少锁竞争来避免
  • 抢占式上下文切换:线程因分配的时间片用尽而被迫放弃CPU或者被其他优先级更高的线程所抢占,一般由于线程数大于CPU可用核心数引起,可通过调整线程数,适当减少线程数来避免。
  1. 无锁并发编程。锁竞争时会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据。
  2. 减少锁的使用,能用CAS代替锁时尽量用CAS代替。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。多个线程使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试(只要cpu分配给线程的时间片没有过,就可以不断的重试,但是时间片过后,如果还是没有成功,也会进行上下文切换,所以说只是减少了上下文切换)。
  3. 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  4. 使用协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

状态

  • New创建:新创建了一个线程对象,但还没有调用start()方法。
  • Runnbale可运行:线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  • Blocked阻塞:表示线程阻塞于锁。
  • Waiting等待:不会被分配CPU执行时间,它们要等待被显式地唤醒(通知或中断),否则会处于无限期等待的状态。
  • Timed Waiting超时等待:不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
  • Terminated终止:线程执行完毕。线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。

调度器

线程的调度策略采用抢占式,优先级高的线程比优先级低的线程优先执行。

    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;
 
   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;
 
    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;
	pubilc final void setPriority(int newPriority);

	public final int getPriority();

线程调度器负责为runnable状态线程分配cpu时间。

同步机制

(我选择学习并发的时候再深入。。。)

同步队列

线程同步机制
临界区

一个访问共用资源的程序片段,这些共用资源无法同时被多个线程访问

互斥量mutex

互斥量的本质就是一把,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。对互斥量加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞知道线程释放该互斥量。

信号量

使用volatile来修饰一个信号量控制线程的中断。

class MyThread implements Runnable {
	private volatile boolean stopMark = false;
    
	@Override
	public void run() {
		System.out.println("thread start");
		while(!stopMark) {
			try {
				Thread.sleep(500);	//模拟一个耗时的方法
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("thread stop");
	}
    
	public void stop() {
		this.stopMark=true;
	}
}
事件

选择器epoll()?NIO?

线程安全
无状态(没有共享变量)

无状态对象一定是线程安全的。

final(如果该引用变量也引用了其他对象,需加锁)

不可变对象一定是线程安全的

容器ConcurrentHashMap

放到容器里再说~

可见性volatile

volatile一般用在多个线程访问同一变量时,对该变量进行唯一性约束,volatile保证了变量的可见性,不能保证原子性。

private volatile booleanflag = false;

保证变量的可见性:volatile本质是告诉JVM当前变量在线程寄存器(工作内存)中的值是不确定的,需要从主存中读取,每个线程对该变量的修改是可见的,当有线程修改该变量时,会立即同步到主存中,其他线程读取的是修改后的最新值。

不能保证原子性:原子性指的是不会被线程调度机制打断的操作,在java中,对基本数据类型的变量的读取和赋值操作是原子性操作。自增/自减操作不是原子性操作。例如:i++,其实是分成三步来操作的:1)从主存中读取i的值;2)执行+1操作;3)回写i的值。volatile关键字并不能保证原子性操作。非原子操作都会产生线程安全的问题。

加锁机制(内置锁,显式Lock)
  1. 内置锁synchronized(以Class对象为锁)

    该关键字能够保证代码块的同步性和方法层面的同步。

    monitorentermonitorexit指令(代码块)

    方法修饰符ACC_SYNCHRONIZED(方法):static方法是类锁,非static方法是对象锁

    不能传参→不能设置超时等待。

    依赖于jvm,编译期保证锁的加锁与释放(非公平锁:不能保证进入访问等待的线程的先后顺序)

    优化:引入偏向锁,轻量级锁

  2. 重入(粒度为线程)

    Lock(ReenTrantLock):基于jdk,手动声明来加锁和释放

    细粒度、灵活

    公平/非公平锁

    Conditon分组唤醒需要唤醒的线程

内置机制(原子类)(AtomicLong…)

不适用多个相互约束变量

保证自增/自减的原子性?
  • 使用java.util.concurrent包下提供的原子类,如AtomicInteger、AtomicLong、AtomicReference等。
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.getAndIncrement();	//实现原子自增
atomicInteger.getAndDecrement();	//实现原子自减
  • 使用synchronized同步代码块
synchronized(this){
      value++;
}
  • 使用Lock显示锁同步代码块
private Lock lock = new ReentrantLock();

private final int incrementAndGet(){
	lock.lock();
    try {
        return value++;
    } finally {
        // TODO: handle finally clause
        lock.unlock();
    }
}
posted @ 2019-08-29 14:26  白芷呀  阅读(136)  评论(0编辑  收藏  举报