多线程基础
一、线程、进程、纤程
进程:OS分配资源的基本单位。OS会为每一个进程独立分配一部分资源,每个进程的资源都有独立的内存空间。每个进程对应一个端口号。
线程:CPU调度的基本单位。共享进程的内存空间,线程没有独立的内存空间。
纤程:相当于用户空间的线程。纤程存在与用户态,不与内核打交道,所以速度会更快,且占用资源比较少。
二、创建线程的方式
Thread:继承Thread类并重写run方法。
class MyThread01 extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "--" + i); } } public static void main(String[] args) { // 通过Thread.start方法启动线程 new MyThread01().start(); } }
Runnable:实现Runnable接口,重写run方法
class MyThread02 implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "--" + i); } } public static void main(String[] args) { // 通过Thread.start方法启动线程 new Thread(new MyThread02()).start(); } }
线程池:
corePoolSize
:核心池的大小。默认情况下,在创建线程池后,线程池的线程数为0,当有新任务时,就会创建一个线程去执行任务,当线程池中的线程数量达到corePoolSize
后,就会把到达的任务放到缓存队列当中。maximumPoolSize
:线程池的最大线程数,表示线程池中最多能创建多少个线程。keepAliveTime
:表示线程没有任务执行时最多存活时间。默认情况下,只有当线程数大于maximumPoolSize
时,keepAliveTime
才会起作用。unit
:参数keepAliveTime
的时间单位。workQueue
:一个阻塞队列,用于存储等待执行的任务。threadFactory
:线程工厂,用来创建线程。handler
:表示当拒绝任务时的策略,有四种取值:ThreadPoolExecutor.AbortPolicy
:丢弃任务并抛出RejectedExecutionException
异常。ThreadPoolExecutor.DiscardPolicy
:也是丢弃任务,但是不抛出异常。ThreadPoolExecutor.DiscardOldestPolicy
:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)ThreadPoolExecutor.CallerRunsPolicy
:由调用线程处理该任务使用线程池的几个优点:
ThreadPoolExecutor
创建线程池:ThreadPoolExecutor
提供了四个构造器public class ThreadPoolExecutor extends AbstractExecutorService{ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } }
参数:
示例:
class MyThread03{ public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // 核心线程数 4, // 最大线程数 5000, TimeUnit.MILLISECONDS, // 线程没有执行任务时最多存活时间 new LinkedBlockingDeque<>() ); for (int i = 0; i < 10; i++) { executor.execute(new ThreadTask()); } } private static class ThreadTask implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()); try { TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
Java通过
Executors
提供四种线程池(不推荐,缓存队列LinkedBlockingQueue
没有设置固定容量大小, 并发量多的时候容易发生OOM),分别是:newCachedThreadPools
创建一个可缓存线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,若无可回收则创建线程。newFixedThreadPools
创建一个定长线程池,可控制最大线程数,超出的线程在队列中等待。newScheduledThreadPools
创建一个定长线程池,支持定时及周期性任务执行。newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序(FIFO,LIFO,优先级)执行。降低资源消耗:通过重用已经创建好的线程来减少创建线程时的消耗。
提高响应速度:任务到达时不需要等待线程创建就可以立即执行。
提高线程的客观理性:线程可以统一管理、分配、调优和监控。
三、sleep()
, yield()
, join()
Thread.sleep(long millis)
:用来暂停当前线程的执行,会通知线程调度器把当前线程在指定的时间周期内置为wait状态。当wait时间结束,线程重新变为Runnable
并等待CPU的再次调度,所以sleep
的实际时间取决于线程调度器,这是由操作系统来完成的。Thread.join()
:很多情况下,主线程生成并启动了子线程,如果子线程需要大量的耗时的运算,主线程往往将于子线程之前结束,但如果主线程处理完其他的事物后,需要用到子线程的处理结果,也就是主线程等待子线程完成之后再结束,这个时候就用到join()
方法了。Thread.yield()
:暂停当前线程, 以便其他线程有机会执行。示例:
public class ThreadDemo02 { public static void main(String[] args) { Thread thread = new Thread(new MyThread(), "MyThread"); thread.start(); for (int i = 0; i < 10; i++) { System.out.println("mian--" + i); try { Thread.sleep(500); if(i == 2) { // 暂停该线程 Thread.yield(); } if(i == 5) { // 等待thread线程执行完成再执行 thread.join(); } } catch (InterruptedException e) { e.printStackTrace(); } } } private static class MyThread implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "--" + i); } } } }
四、线程的状态
+-------------->TIMED_WAITED---------------------+ |Thread.sleep(Long) | Object.notify() |Object.wait(Long) | Object.notifyAll() |Thread.join(Long) | LockSupport.unpark(Thread) |LockSupport.parkNanos() | |LockSupport.parkUntil() | | (RUNNABLE) v +-----------------------------------------------------+ | | | +-------+ system scheduling +---------+ | +-----------+ +-----+ | | | -------------------------> | | | | | | NEW |--start()-->| | READY | | RUNNING | |---------->| TEMINATED | +-----+ | | | <----Thread.yield---------- | | | | | | +-------+ + --------+ | +-----------| +-----------------------------------------------------+ | |Ojbect.wait() Ojbect.notify() ^ ^ | |Thread.join() Ojbect.notifyAll() | |获取到锁 | |LocKSupport.park() LocKSupport.unpark(Thread)| | | +-------------------->WAIT--------------------+ | +----------------------->BLOCKED--------------------+ 等待进入Synchronized方法(块)
NEW(初始):通过实现Runnable或继承Thread得到一个线程类,new一个实例出来,线程就进入了初始状态。
READY(就绪):线程进入就绪状态说明该线程有了运行资格,可以通过系统的调度进入运行状态。
调用线程的
start()
方法,该线程就进入了就绪状态。线程的
sleep()
方法结束,或者其他方法join()
结束,等待用户输入结束,或者某个线程拿到锁,线程进入就绪状态。调用当前线程
yield()
方法,当前线程进入就绪状态。锁池中的对象拿到对象锁之后,进入就绪状态。
RUNNING(运行):线程调度程序从可运行池中选择一个线程作为当前线程所处的状态。这也是线程进入运行状态的唯一一种方式。
BOLCKED(阻塞):组赛状态是线程阻塞进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
WAIT(等待):这种状态的线程不会被分配CPU执行时间,它们要显式的被唤醒,否则会进入无期限等待的状态。
TIMED_WAITING(超时等待):这种状态的线程不会被分配CPU执行时间 ,不过它们无须被唤醒,在达到一定时间它们会被自动唤醒。
TEMINATED(终止):线程的run方法或者主线程的main方法执行完成之后,这个线程就终止了。终止的线程不能复生。如果强行调用
start()
方法执行线程,会抛出java.lang.IllegalThreadStateException
异常。
五、synchronized
关键字
既可以保证线程的原子性,又可以保证线程的可见性
先看下面这个程序:
package com.kangfawei.item01;import java.util.concurrent.TimeUnit;public class ThreadDemo03 { public static void main(String[] args) { UnSyncDemo unSyncDemo = new UnSyncDemo(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(unSyncDemo); thread.start(); } try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(unSyncDemo.count); } private static class UnSyncDemo implements Runnable { int count = 0; @Override public void run() { for (int i = 0; i < 1000; i++) { count++; } } } }
上面程序中开启了10个线程,每个线程都累加了1000次,结果应该时10*1000=10000,但是多次执行下来发现结果都是比10000小,有些甚至少很多。原因就是每个线程都有不同的栈空间,一个线程将count中的数据拿到之间的栈空间执行但还没有执行完成的时候,另外一个线程拿到同样结果的count在自己的空间中执行,这样就会导致之前的情况发生。
设想如果多个线程依次执行的时候就不会产生上面的那种情况。java中的synchronized
关键字就是让每个线程依次执行,但是同时,synchronized
关键字由于每个线程依次执行的原因会很明显的降低程序的效率。
package com.kangfawei.item01;import java.util.concurrent.TimeUnit;public class ThreadDemo03 { public static void main(String[] args) { SyncDemo syncDemo = new SyncDemo(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(syncDemo); thread.start(); } try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(syncDemo.count); } private static class SyncDemo implements Runnable { int count = 0; @Override public void run() { synchronized (this) { // 增加的代码块加锁 for (int i = 0; i < 1000; i++) { count++; } } } } }
此时不管执行多少次,执行结果都会是10000,因为前面执行的线程会占用对象锁,等待执行完成之后才会释放锁,其他线程抢到锁的时候继续占有锁,剩下的线程继续等待,直到所有线程执行完毕。这样就不会产生多个线程拿到同样的count执行。
锁的对象:
实例方法中:
public synchronized void mothed()
实例方法中锁的对象是该类的实例对象静态方法中:
public static synchronized void methor()
静态方法中所得对象是类对象实例对象:
synchronized(this) {}
同步代码块,锁住的是该类的实例对象class对象:
synchronized(SyncDemo.class) {}
同步代码块,锁住的是该类的对象任意Object:
synchronized(obj) {}
任意对象可以作为同步代码块的锁(谨慎使用String、Integer等包装类)可重入
同一个锁程中,线程可以多次获取同一把锁。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一,当计数器为零的时候释放锁。
锁升级
synchronized
锁可以升级,无锁-->偏向锁-->自旋锁-->OS锁没有线程访问同步方法/同步代码块的时候,是没有锁的。当只有一个线程访问的时候,由无锁升级为偏向锁,当锁发现访问的还是同一个线程的时候,默认放行。继续增加线程的时候,锁会升级为自旋锁,线程会循环获取锁,如果线程多次循环还是无法获取到锁,或者等待线程过多,那么锁就会升级为最终的OS锁,线程会进入OS的就绪队列中,直到锁被释放后去抢到锁。
自旋锁:效率高,但是自旋过程会占用CPU资源,所以线程数比较小、时间比较短的情况下适合使用。
重量级锁:不占用CPU资源,但是效率比较低
六、volatile
关键字
保证线程可见性
public class ThreadDemo04 { public static void main(String[] args) { VolatileTest test = new VolatileTest(); Thread thread = new Thread(test, "T1"); thread.start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 2s过后更改flag的值,让线程停止运行 test.flag = false; } }class VolatileTest implements Runnable{ // 此处volatile必须要加,如果不加会导致更改flag变量此处接收不到 volatile boolean flag = true; @Override public void run() { while(flag) { // 通过更改变量flag让线程停止 // 如果不加volatile线程会一直向下运行 } } }
由于上面两条规则,所以
volatile
可以保证每次看到的都会是最新值,从而保证可见性。但是volatile
不保证线程的原子性,所以不可以保证线程的安全性。volatile
关键字每次读取变量前必须先从主内存刷新最新的值。volatile
关键字写入后必须同步会主内存中禁止指令重排序
在一段代码的运行过程中,对于一些没有依赖关系的代码,编译器和处理器为了提高效率,可能会产生一些顺序的改变,比如:
int a = 1; // 1int b = 2; // 2int c = a + b; // 3
在上面的代码中,代码1跟2没有依赖关系,所以处理器执行过程中可能会先执行2,然后再执行1。在单线程中,这种指令重排序不会产生任何问题,但是再多线程的情况下,可能就会有一些问题出现。如在下面的懒加载单例模式中:
class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if(instance == null) { synchronized (Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return instance; } }
在懒加载的单例模式中,上面的例子几乎可以说是完美的,一般情况下不会由问题产生,但这个问题并不是百分百完美的,在一些并发量极大的情况下,会发生instance不为null,但是变量没有被初始化。
instance == new Singleton()
这个赋值语句并不是一个原子操作,它可以分为三部分执行:当发生指令重排序的时候,第二步跟第三步调换位置,instance指向分配好的内存空间的时候,这个内存空间没有被初始化。如果此时另外一个线程进入,就会直接返回没有被初始化内存空间的对象。解决这个问题就需要将声明加上
volatile
:private static volatile Singleton instance;
分配内存空间
初始化对象
设置instance执行刚分配的内存地址
0条评论
点击登录参与评论