什么是线程?
线程是 cpu调度和执行的单位。
多个线程共享进程的堆 和方法区 资源,但每个线程有自己的程序计数器 、虚拟机栈 和本地方法栈。
如何实现线程
继承Thread类
实现步骤:
- 创建自定义类,继承Thread类
- 重写run方法
- 创建自定义类对象,并.start()启动线程
javaclass Th extends Thread { @Override public void run() { for (int i = 0; i < 2000; i++) { System.out.println(i + "Th"); } } public static void main(String[] args) { Th th = new Th(); th.start(); } }
实现Runnable接口
实现步骤 :
- 创建自定义类,实现Runnable接口
- 重写run()方法
- 创建Thread对象,把自定义类作为参数传进去
javaclass Run implements Runnable{ @Override public void run() { for (int i = 0; i < 2000; i++) { System.out.println("Run"+i); } } public static void main(String[] args) { new Thread(new Run()).start(); } }
实现Callable接口
目的就是为了来处理 Runnable 不支持的用例。 Runnable 接口 不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口 ,这样代 码看起来会更加简洁。
实现步骤 :
- 创建自定义类,实现Callable接口
- 重写call()方法
- 创建自定义类对象
- 创建执行服务(线程池)
- 提交服务
- 获取结果
- 关闭服务
javaclass Cal implements Callable<Boolean> { @Override public Boolean call() throws Exception { return null; } public static void main(String[] args) throws Exception { Cal cal = new Cal(); Cal cal1 = new Cal(); //创建执行服务 ExecutorService executorService = Executors.newFixedThreadPool(2); //提交服务 Future<Boolean> future = executorService.submit(cal); Future<Boolean> future1 = executorService.submit(cal1); //获取结果 Boolean b = future.get(); System.out.println(b); Boolean b1 = future1.get(); System.out.println(b1); //关闭服务 executorService.shutdown(); } }
线程的状态
线程创建之后它将处于 NEW (初始) 状态,调用 start() 方法后开始运行,线程这时候处于 READY (可运行) 状态。可运行状态的线程获得了 CPU 时间片( timeslice )后就处于 RUNNING (运 行) 状态。
当线程执行 wait() 方法之后,线程进入 WAITING (等待) 状态。进入等待状态的线程需要依靠其他 线程的通知才能够返回到运行状态,而 TIME_WAITING( 超时等待 ) 状态相当于在等待状态的基础上增加 了超时限制,比如通过 sleep ( long millis ) 方法或 wait ( long millis ) 方法可以将 Java 线程置 于 TIMED WAITING (超时等待) 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE (运行) 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED (阻塞) 状态。
线程在执行 Runnable 的 run() 方法之后将会进入到 TERMINATED (终止) 状态。
线程执行 wait() 方法之后线程进入 Waiting 状态,由于调用 wait() 时会释放占用的 cpu 资源和锁,所以 当Waiting 线程被其他线程调用 Object.notify() 唤醒之后,需要重新获取对象上的锁。这时候也会先进入 Blocked状态等待获取锁。sleep和wait****有什么区别?
- wait 必须搭配 synchronize 一起使用,而 sleep 不需要;
- 进入 wait 状态的线程能够被 notify 和 notifyAll 线程唤醒,而 sleep 状态的线程不能被 notify 方法 唤醒;
- wait 通常有条件地执行,线程会一直处于 wait 状态,直到某个条件变为真,但是 sleep 仅仅让线 程进入睡眠状态;
- wait 方法会释放对象锁,但 sleep 方法不会。
- 在调用 wait 方法之后,线程会变为 WATING(等待)状态,而调用 sleep 方法之后,线程会变为 TIMED_WAITING(超时等待)
ThreadLocal
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过set(T) 方法来设置一个值,在 当前线程下 再通过 get()方法获取到原先设置的值。ThreadLocal 是 线程安全 。
- 在Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个 threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本 (即T类型的变量)。
- 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方 法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以 ThreadLocal要保存的副本变量为value,存到threadLocals。
- 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
ThreadLocal造成内存泄漏的原因?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用 , 而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来, ThreadLocalMap中就会出现 key 为 null 的 Entry 。假如我们不做任何措施的话 ,value 永远无法被 GC 回 收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set() 、 get()、 remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后最好手动调用 remove()方法。
ThreadLocal内存泄漏解决方案?
每次使用完 ThreadLocal ,都调用它的 remove() 方法,清除数据。在使用线程池的情况下,没有及时清理ThreadLocal ,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就需要用完就清理。
线程同步
volatile
volatile 是 Java 虚拟机提供的 轻量级 的同步机制。
volatile 关键字可以保证变量的可见性 ,如果我们将变量声明为 volatile ,这就指示 JVM ,这个变量是共 享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序 。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。synchronized
老版本 Java 中的 synchronized 属于 重量级锁 ,效率低下。 因为监视器锁( monitor )是依赖于底层的 操作系统的 Mutex Lock 来实现的, Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者 唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核 态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。在 JDK6 之后, Java 官方从 JVM 层面对 synchronized 进行了较大优化。对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized 关键字解决的是多个线程之间访问资源的同步性, synchronized 关键字可以保证被它修饰 的方法或者代码块在任意时刻只能有一个线程执行。synchronized控制对对象的访问,每个对象一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞;方法一旦执行就独占该线程,直到该方法返回才释放锁,后面被阻 塞的线程才可以获得锁,继续执行。
synchronized****的使用
修饰实例方法
给 当前对象实例 加锁,进入同步代码前要获得 当前对象实例的锁 。
javapublic synchronized void buy() { //同步实例方法 }
修饰静态方法
给 当前类 加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁 。这是因为静态成 员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
javapublic static synchronized void method(){ //同步静态方法 }
静态 synchronized 方法 和 非静态 synchronized 方法 之间的调用不互斥。因为访问 静态 synchronized
方法 占用的锁是 当前类 的锁,而访问 非静态 synchronized 方法 占用的锁是 当前实例对象 锁。
修饰代码块
synchronized(object) 表示进入同步代码块前要获得 给定对象的锁 。
synchronized(Object.class) 表示进入同步代码前要获得 给定 类的锁 。
javavoid method(){ synchronized (object/Object.class) { //同步代码块 } }
总结
synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁;
synchronized 关键字加到实例方法上是给对象实例上锁;synchronized和volatile****的区别
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
- volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 关键字要 好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。 synchronized 关键字两者都能保证。
- volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
ReentrantLock
ReentrantLock 是一个可重入的互斥锁,又被称为 " 独占锁 " 。在同一个时间点只能被一个线程锁持有;可重入表示,ReentrantLock 可以被同一个线程多次获取。
ReentraantLock 是通过一个 FIFO 的等待队列来管理获取该锁所有线程的。在 " 公平锁 " 的机制下,线程依次排队获取锁;而" 非公平锁 " 在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。
javaclass Book implements Runnable { private ReentrantLock lock = new ReentrantLock(); static private Integer n = 3; @Override public void run() { lock.lock(); if (n <= 0) { lock.unlock(); return; } n--; lock.unlock(); } }
synchronized和ReentrantLock****的区别
两者都是可重入锁。 可重入锁 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1 ,所以要等到锁的计数器下降为 0 时才能释放锁。
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API 。synchronized 是依赖于 JVM 实现的, ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合try/finally 语句块来完成),所以可以通过查看它的源代码,来看它是如何实现的。
- ReentrantLock 比 synchronized 增加了一些高级功能
- 等待可中断 : ReentrantLock 提供了通过 lock.lockInterruptibly() 来实现能够中断等待锁的线程的机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- 可实现公平锁 : ReentrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock 默认情况是非公平的,可以通过 ReentrantLock 类的 ReentrantLock(boolean fair) 构造方法来制定是否是 公平的。
- 可实现选择性通知(锁可以绑定多个条件): synchronized 关键字与 wait() 和 notify() / notifyAll() 方法相结合可以实现等待/通知机制。 ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。
线程池
为什么使用线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
创建线程
通过构造方法实现
javapublic class Outer { private static final int CORE_POOL_SIZE = 5; private static final int MAX_POOL_SIZE = 10; private static final Long KEEP_ALIVE_TIME = 1L; private static final int QUEUE_CAPACITY = 100; public static void main(String[] args) { //创建线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY), new ThreadPoolExecutor.AbortPolicy() ); Runnable runnable = new Book(); for (int i = 0; i < 10; i++) { executor.execute(runnable); } executor.shutdown(); while (!executor.isTerminated()) { } } } class Book implements Runnable { @Override public void run() { } }
通过Executor框架的工具类Executors来实
创建三种类型的 ThreadPoolExecutor :
- newFixedThreadPool: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
javapublic class Outer { public static void main(String[] args) { //创建线程池 ExecutorService executor=Executors.newFixedThreadPool(5); Runnable runnable = new Book(); for (int i = 0; i < 10; i++) { executor.execute(runnable); } executor.shutdown(); while (executor.isTerminated()){ } } }
- newSingleThreadExecutor : 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
javapublic class Outer { public static void main(String[] args) { //创建线程池 ExecutorService executor = Executors.newSingleThreadExecutor(); Runnable runnable = new Book(); for (int i = 0; i < 10; i++) { executor.execute(runnable); } executor.shutdown(); while (executor.isTerminated()) { } } }
- newCachedThreadPool**:**该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
javapublic class Outer { public static void main(String[] args) { //创建线程池 ExecutorService executor = Executors.newCachedThreadPool(); Runnable runnable = new Book(); for (int i = 0; i < 10; i++) { executor.execute(runnable); } executor.shutdown(); while (executor.isTerminated()) { } } }
Executor****框架
在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 this 逃逸是指在构造函 数返回之前其他线程就持有该对象的引用 . 调用尚未构造完全的对象的方法可能引发令人疑惑的错误 。 Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等, Executor 框架让并发编程变得更加简单。
Executor框架结构(主要由三大部分组成)
- 任务。执行任务需要实现Runnable接口或Callable接口。
- 任务的执行。任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。 Executor框架ThreadPoolExecutor和ScheduledThreadPoolExecutor这两个关键类实现了ExecutorService接口。实际上我们需要更多关注的是ThreadPoolExecutor 这个类,这个类在****我们实际使用线程池的过程中,使用频率还是非常高的。
- 异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
Executor****框架的使用
- 主线程首先要创建实现Runnable 或者Callable 接口的任务对象。
- 把创建完成的实现Runnable **/**Callable 接口的 对象直接交给ExecutorService 执行: ExecutorService.execute(Runnable command) )或者也可以把 Runnable 对象或 Callable 对象提交给 ExecutorService 执行( ExecutorService.submit(Runnable task) 或 ExecutorService.submit(Callable <T> task) )。
- 如果执行ExecutorService.submit(...) **,**ExecutorService 将返回一个实现Future 接口****的对象(我们刚刚也提到过了执行 execute() 方法和 submit() 方法的区别, submit() 会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable ,我们也可以创建FutureTask ,然后直接交给 ExecutorService 执行。
- 最后,主线程可以执行FutureTask.get() 方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning) 来取消此任务的执行。