Java中线程的理解

Thread

Thread的实现和原理
  • 为什么要重视线程?因为同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器,线程切换开销小。(线程是cpu调度的最小单位)是并发处理的关键。
  • 线程的生命周期有五个阶段:创建、就绪、运行、阻塞、终止。
    • 创建状态(New):新创建了一个线程对象。
    • 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
    • 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
    • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
      (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
      (二)、同步阻塞:运行的线程在获取对象的(synchronized)同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
      (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
    • 终止状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
  • 多线程是指在同一程序中有多个计算在同时执行。在java中要想实现多线程,有三种方式,一种是继承Thread类,一种是实现Runable接口,另外一种是实现Callable接口,并与Future、线程池结合使用。现实场景上我们用最多的应该是第三种,而且在特殊的业务场景下,我们还需要去自定义线程池。下面我们先看下实现线程的三种方式:
继承Thread
class DemoThread extends Thread{
	private String name;
    public DemoThread(String name) {
       this.name=name;
    }
	public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行  :  " + i);
            try {
                sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
	}
}
public class Demo {
	public static void main(String[] args) {
		DemoThread thread1=new DemoThread("A");
		DemoThread thread2=new DemoThread("B");
		thread1.start();
		thread2.start();
	}
}

程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用DemoThread的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在一个进程下多线程下运行。这个时候此线程的生命周期是new的时候创建了两条线程,start()方法的调用后使得该线程变为可运行态(Runnable),由操作系统决定什么时候运动(乱序执行,谁先谁后不一定),运行后开始遍历循环执行打印,Thread.sleep()方法调用阻塞1秒,目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会,最后遍历完终止。

实现Runable接口
class DemoThread implements Runnable{
	private String name;
	public DemoThread(String name) {
		this.name=name;
	}
	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
	    	System.out.println(name + "运行  :  " + i);
	        try {
		        Thread.sleep(1000L);
	        } catch (InterruptedException e) {
		        e.printStackTrace();
	        }
		}	
	}
}
public class Demo{
	public static void main(String[] args) {
		new Thread(new DemoThread("A")).start();
		new Thread(new DemoThread("B")).start();
	}
}

和继承Thread一样的原理(Thread类实际上也是实现了Runnable接口的类)。但这里Runnable执行的时候可以发现其实还有有很大不同,继承Thread一个是多个线程分别完成自己的任务,Runnable执行一个是多个线程共同完成一个任务。也就是实现Runnable是可以达到多个相同的程序代码的线程去处理同一个资源,这也是为什么线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类的原因。再加上 java单继承的限制,所以相对来说Runnable在代码开发中是更健壮更好用的。但无论是使用Runnable接口、Thread实现的线程我们都是无法返回结果的。都是异步的。

  • Thread类有几个常用的方法
    • sleep(): 强迫一个线程睡眠N毫秒。
    • isAlive(): 判断一个线程是否存活。
    • join(): 等待线程终止。
    • activeCount(): 程序中活跃的线程数。
    • activeCount(): 程序中活跃的线程数。
    • enumerate(): 枚举程序中的线程。
    • currentThread(): 得到当前线程。
    • isDaemon(): 一个线程是否为守护线程。
    • setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
    • setName(): 为线程设置一个名称。
    • wait(): 强迫一个线程等待。
    • notify(): 通知一个线程继续运行。
    • setPriority(): 设置一个线程的优先级。
实现Callable

当然很多情况下我们也是需要拿到返回结果的,这时候就需要使用用Callable、Future、FutureTask、CompletionService这几个类。Callable只能在ExecutorService的线程池中跑,但有返回结果,也可以通过返回的Future对象查询执行状态取得异步任务的结果。

class DemoHandleFuture<Integer> implements Callable<Integer> {
	private Integer num;
	public DemoHandleFuture(Integer num) {
		this.num = num;
	}
	@Override
	public Integer call() throws Exception {
		Thread.sleep(3*100);
		System.out.println(Thread.currentThread().getName());
		return num;
	}	
}
public class Demo{
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService executor = Executors.newCachedThreadPool();
		CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executor);
		for (int i = 0;i<5;i++) {
			completionService.submit(new DemoHandleFuture(i));
		}
		for (int i = 0;i<10;i++) {
			System.out.println("返回:"+completionService.take().get());
		}
	}
}

这里结合下源码看下,

public interface Callable<V> {
    V call() throws Exception;
}
public interface Future<V> {
  /**
    * 试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,
    * 则此尝试将失败。当调用 cancel 时,如果调用成功,而此任务尚未启动则此任务将永不运行。
    * 如果任务已经启动,则 mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式
    * 来中断执行此任务的线程。此方法返回后,对 isDone() 的后续调用将始终返回 true。如果
    * 此方法返回 true,则对 isCancelled() 的后续调用将始终返回 true。 
    **/
   boolean cancel(boolean mayInterruptIfRunning);
   
   /** 如果在任务正常完成前将其取消,则返回 true。 **/
   boolean isCancelled();
 
   /** 如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,
    *  此方法都将返回 true。
    **/
    boolean isDone();
 
    V get() throws InterruptedException, ExecutionException;
 
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
/**
 * FutureTask类是Future 的一个实现,并实现了Runnable,所以可通过Excutor(线程池) 来执行,
 * 也可传递给Thread对象执行。如果在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,
 * 可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台
 * 作业的计算结果或者执行状态。 Executor框架利用FutureTask来完成异步任务,并可以用来进行
 * 任何潜在的耗时的计算。一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去
 * 获取结果。FutureTask类既可以使用new Thread(Runnable r)放到一个新线程中跑,也可以使
 * 用ExecutorService.submit(Runnable r)放到线程池中跑,而且两种方式都可以获取返回结果,
 * 但实质是一样的,即如果要有返回结果那么构造函数一定要注入一个Callable对象。
 **/
public class FutureTask<V> implements RunnableFuture<V> {
    //真正用来执行线程的类
    private final Sync sync;
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        sync = new Sync(callable);
    }
    public FutureTask(Runnable runnable, V result) {
        sync = new Sync(Executors.callable(runnable, result));
    }
    public boolean isCancelled() {
        return sync.innerIsCancelled();
    }
    public boolean isDone() {
        return sync.innerIsDone();
    }
    public boolean cancel(boolean mayInterruptIfRunning) {
        return sync.innerCancel(mayInterruptIfRunning);
    }
    public V get() throws InterruptedException, ExecutionException {
        return sync.innerGet();
    }
    public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        return sync.innerGet(unit.toNanos(timeout));
    }
    //线程结束后的操作
    protected void done() { }
    //设置结果
    protected void set(V v) {
        sync.innerSet(v);
    }
    //设置异常
    protected void setException(Throwable t) {
        sync.innerSetException(t);
    }
    //线程执行入口
    public void run() {
        sync.innerRun();
    }
    //重置
    protected boolean runAndReset() {
        return sync.innerRunAndReset();
    }
    //这个类才是真正执行、关闭线程的类
    private final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -7828117401763700385L;
        //线程运行状态
        private static final int RUNNING   = 1;
        private static final int RAN       = 2;
        private static final int CANCELLED = 4;
        
        private final Callable<V> callable;
        private V result;
        private Throwable exception;
 
        //线程实例
        private volatile Thread runner;
        //构造函数
        Sync(Callable<V> callable) {
            this.callable = callable;
        }
      ......
    }
}
// 这里直贴接口实现大家自行看源码哈~
public interface CompletionService<V> {
   //提交线程任务
   Future<V> submit(Callable<V> task);
   //提交线程任务
   Future<V> submit(Runnable task, V result);
   //阻塞等待
   Future<V> take() throws InterruptedException;
   //非阻塞等待
   Future<V> poll();
   //带时间的非阻塞等待
   Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}
Thread调度
  • 优先级
    • 说到调度就肯定会有优先级,但我们也看到最终线程也会被代码呼唤到就绪状态(Runnable),运行还是取决于操作系统的,但其实这里会有个概率问题,Java线程是可以设置优先级的,优先级高的线程会获得较多的运行机会。Java线程的优先级用整数表示,取值范围是1~10,源码本身提供了三个静态常量优先级:最低最高和中间。Thread类是通过setPriority()方法设置优先级,不设置的话默认优先级为中间(Thread.NORM_PRIORITY == 5)。如果线程被继承,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
  • 睡眠
    • 其实就是Thread.sleep(long millis)方法,使线程转到阻塞状态,而且睡眠下的线程无法唤醒或任何操作使其重新就绪的。但睡眠结束后线程又会变成就绪状态(Runnable),等待操作系统的去执行。
  • 等待
    • 其实就是 java对象必带的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。
  • 唤醒
    • 其实就是 java对象必带的notify()方法。唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
  • 让步
    • Thread.yield() 方法,让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。目的是让相同优先级的线程之间能适当的轮转执行。但这里有个有意思的问题,如果都在一个优先级让步了但也不能保证等会又是我运行啊…
  • 加入
    • join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。一般是有坑的情况下才用,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
Thread同步
  • 使用 synchronized 关键字。synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter、monitorexit 两条指令。而且synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。然后我们看下这两条指令是做了什么
    • 在执行 monitorenter 指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加 1
    • 在执行 monitorexit 指令时会将锁计数器减 1,当计数器为 0 时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
  • 除了 synchronized 之外,我们还可以使用 java.util.concurrent(简称 J.U.C)包中的重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock 与 synchronized 很相似,他们都具备一样的线程重入特性,不同点表现在代码写法上,ReentrantLock 需要在带上执行lock() 和 unlock() 方法配合 try/finally 语句块来完成。且 ReentrantLock 增加了几个功能(不过在不是特定情况下还是喜欢用synchronized)
    • 等待可中断,如果持有锁的线程长时间不释放锁,正在等待的线程可以选择放弃等待,改为处理其他事情。
    • 公平锁
      • 多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;
      • 非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
      • ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
    • 锁可以绑定多个条件
      • 一个 ReentrantLock 对象可以同时绑定多个 Condition 对象
      • 而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁。
线程数据传递

在上诉的 Demo 里可以看出线程数据传递是可以通过构造方法传递数据的,也可以通过变量、对象和方法传递数据。这里比较简单不做解释,看下多线程之间传递数据:

public class Demo{
	public static void main(String[] args) {
		ExecutorService service = Executors.newCachedThreadPool();
		// Exchanger是一种线程间安全交换数据的机制
		final Exchanger exchanger = new Exchanger();
		service.execute(new Runnable(){
			@Override
			public void run() {
				try {				
					String data1 = "Hello";
					System.out.println("运行  :  " + Thread.currentThread().getName() + ",交换:" + data1);
					Thread.sleep(1000L);
					String data2 = (String)exchanger.exchange(data1);
					System.out.println("运行  :  " + Thread.currentThread().getName() + ",返回:" + data2);
				}catch(Exception e){
					e.printStackTrace();
				}
			}	
		});
		service.execute(new Runnable(){
			@Override
			public void run() {
				try {
					String data1 = "Hi";
					System.out.println("运行  :  " + Thread.currentThread().getName() + ",交换:" + data1);
					Thread.sleep(1000L);					
					String data2 = (String)exchanger.exchange(data1);
					System.out.println("运行  :  " + Thread.currentThread().getName() + ",返回:" + data2);
				}catch(Exception e){
					e.printStackTrace();
				}				
			}	
		});
	}
}

运行得到:

运行  :  pool-1-thread-1,交换:Hello
运行  :  pool-1-thread-2,交换:Hi
运行  :  pool-1-thread-2,返回:Hello
运行  :  pool-1-thread-1,返回:Hi

Exchanger(交换者)是一个用于线程间数据交换协作的工具类。它提供一个同步点,在这个同步点多个线程间两两之间线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
其原理是根据每个线程的ID,hash计算出自己所在的slot index,如果这个slot被人占着(slot里面有node),并且有人正在等待交换,那就和它进行交换。如果slot为空的(slot里面没有node),自己占着,等人交换。没人交换,向前挪个位置,把当前slot里面内容取消,index减半,再看有没有交换。然后再挪到0这个位置,还没有人交互,那就阻塞,一直等着。别的线程,也会一直挪动,直到0这个位置(终点)。
源码只提供我们三个公共方法:

  • Exchanger():无参构造方法
  • exchange(V):exchange方法用于交互数据V
  • exchange(V,long,TimeUnit):延迟一定时间交换数据
线程安全
  • 怎样算安全?当多个线程访问一个对象时,如果没有其他特殊线程操作下,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。
  • 怎么做到安全?java中线程安全的实现方法主要有以下三种:互斥同步、非阻塞同步、无同步。
    • 互斥同步:同步是指在多个线程并发访问共享数据的时候,保证共享数据在同一时刻只能有一个(或者一些,使用信号量的时候)线程使用。临界区、互斥量、信号量都是主要的互斥实现方式。最基本的互斥同步实现方法就是使用 synchronized 关键字。
    • 非阻塞同步:这种在没有硬件支持情况下我觉得是做不到的,不服来辩
    • 无同步:每一个线程的 Thread 对象中都有一个 ThreadLocalMap 对象,这个对象存储了一组以 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值的 K-V 值对,ThreadLocal 对象就是当前线程的 ThreadLocalMap 的访问入口,每一个 ThreadLocal 对象都包含了一个独一无二的 threadLocalHashCode 值,使用这个值就可以在线程 K-V 值对中找回对应的本地线程变量。
  • 安全又会涉及到锁的问题,怎么用怎么优化,下次有空再说了~

本文纯属个人总结,如有错误请矫正,还有本文中为了demo才没有手动创建线程池,现实场景中请手动创建线程池规避风险,线程数根据服务器的内存和看服务器是几核的处理器定夺。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值