10.1 基本概念
进程之间内存不共享
Java中线程间堆内存和方法区共享,栈内存独立(很重要),方法区只有1块,堆也只有1块,栈可能有多个,主线程对应主栈,在进程中创建的变量或对象的引用位于栈中,不共享。(多个进程间不共享局部变量)
每个栈与每个栈之间之间互不干扰,各自执行,这就是多线程并发。
堆,栈,方法区
线程的生命周期:
NEW:创建
RUNNABLE:启动线程之后,被唤醒之后,有被CPU调度的机会
WAITING:进入等待集
WAITINGTIME:带阻塞时间的等待
RUNNING:运行态
BLOCKING:阻塞态
TERMINATED:死亡态
线程的运行由CPU调度,每一次运行结束的位置由程序计数器计算,下一次从上一次断开的位置继续。
10.2 创建线程的方法
- 创建线程的方法有很多种:
- extends Thread,重写run()方法
MyThread t = new MyThread()
- implements Runnable,重写run()方法。==Runnable只是一个普通接口==
Runnable r = new MyRunnable(); Thread t = new Thread(r);
- implements Callable,重写call方法。==Callable也只是一个普通接口==
Callable c = new MyCallable(); //用FutureTask去接收Callable,FutureTask是一个有返回值的线程,因为call()方法有返回值 FutureTask f = new FutureTask(c); f.run(); f.get();//get是一个阻塞性方法,会一直获取。也可以指定等待时间
- 传入一个线程Thread1,==Thread类实现了Runnable接口==
Thread t = new Thread(Thread1)
- 传入 FutureTask,==FutureTask实现了RunnableFuture接口,RunnableFuture继承至Runnable==
Thread t = new Thread(new FutureTask(new Callable(){重写call()方法}))
- 线程池ThreadPoolExecutor,在后面介绍
- 线程组ThreadGroup
10.3 线程的常用方法
开启线程
//Thread类 void start(); //FutureTask类 Object run();
注意如果直接调用run()方法不会开启线程。
设置守护线程
//守护线程会等待到所有用户线程结束后自动结束 void setDeamon(true);
- 中断,设置中断标志
/*设置中断标志并不会中断线程,如果处于wait,sleep,join等阻塞态被中断时,会抛出InterruptedException异常 。在catch到异常之后,中断标志重置为false*/ void interrupt(); Thread t ; t.interrupt();
- 检测中断的两个方法
//静态方法 Thread.interrupted();//检测当前线程中断标志位,如果为true,则返回true并重置为false。 //实例方法 void isInterrupted();//检测中断标志,不重置
- 阻塞线程的方法
1.void sleep(long timeMills);//挂起线程持续timeMills时间,单位毫秒 2.void wait() void wait(long timeMills) //需要先获取锁,synchronized,然后用该锁执行wait(),线程将自己挂起并释放锁 3.void join() //将调用者加到当前线程,等待调用者执行完毕。
- 唤醒线程,使线程重新回到Runnable态
//唤醒线程也要先获取锁,synchronized,并且用和wait相同的锁去notify void notify()//随机唤醒单个线程 void notifyAll()//唤醒所有在等待中的线程,只会唤醒在调用notifyAll之前就处于等待的线程。
==notify造成死锁的原因分析==
假如有消费者C1和C2,生产者P1,C1先拿到锁,发现没有产品,就wait,然后释放锁,锁被P1拿到了,就产品+1,然后notify,然后下一次争抢锁,P1抢到了,发现有产品,就wait,然后释放锁,锁被C1或者C2拿到了,消费,然后notify,释放锁,再一次抢夺锁时,只有消费者抢到了,则它就会wait,这把锁只要被消费者抢到,因为没有产品,就只能wait。就进入了死锁。即只要生产者连续两次拿到锁,必然会生产产品,然后第二次将自己挂起,后面只要消费者消费了之后唤醒的不是生产者,就会使得所有消费者都获得锁,然后把自己挂起。最终进入死锁。
所以结论是,所有生产者和消费者用notifyAll一定不会死锁,因为这样会唤醒所有挂起的线程,不存在忽略了生产者而造成后续的死锁。死锁产生的根本原因是生产者只唤醒生产者,消费者只唤醒消费者,最终生产者都认为满了,然后挂起;消费者消费完了,唤醒别的消费者,别的消费者就会挂起,直到最先的消费者被挂起。而如果都是用notifyAll,就不分差异的唤醒所有
- 让出CPU执行权
void yield();//让出当前的CPU执行权,让CPU重新调度,有可能再次被调度到
- 设置线程优先级
void setPriority();//默认等级5,最高10,最低1。优先级越高,被调度到的概率越大
10.4 线程同步
- 线程同步是解决线程安全问题的方法,主要有==synchronized==同步,==Lock==同步和==CAS非阻塞算法==同步。CPU效率依次递增
1、synchronized关键字
synchronized作用于方法内
//synchronized(lock),lock一般是共享的资源,虽然用其他唯一的实例也可以,但是推荐用共享资源 @override public void run(){ synchronized(lock){ //do something } }
synchronized作用于方法上
1.实例方法 public synchronized void method(){} //此时的锁为this,实例对象 2.静态方法 public synchronized static void method(){} //此时的锁为类锁,class,它是唯一的
2、volatile关键字
- volatile只保存内存可见性,它不保证线程安全
- volatile修饰的变量在发生变化时,会及时通知其它线程,重新从主内存中取值到工作内存。但是因为volatile不是原子性操作,因此存在线程安全。
- 总线嗅探机制:线程A修改信息后在汇编指令中会加Lock前缀,修改信息通过总线被线程B嗅探到,(因为cpu和内存的交互通过I\O总线),此时B就把原来内存空间的变量变为无效状态。重新从主内存中读取。
- volatile不保证线程安全的原因:
因为线程A在引擎从工作内存中拿到值之后,读,改是分步的,这时候如果另外一个线程B已经完成了写到主内存的操作,假如写入的值为2。这时候线程A会通过嗅探机制,感知到volatile修改的值的变化,会将自己工作内存的变量舍去,置位无效,但是引擎已经读取了值了,并且完成了值的修改,假如修改为了1。这时候,引擎就会assgin,把值存到工作内存,然后再写到主内存,就会将原来主内存的值给覆盖了。
3、Lock锁
- java.util.concurrent.locks包下的Lock是一个接口,其常用实现类为ReentrantLock,效率比synchronized高,更安全
- Lock锁也只有释放了才能被其它线程获取到。
- ==ReentrantLock==
//它是一个独占锁 new ReentrantLock(boolean fair);//fair表示是否公平 公平:先到先得 不公平:先到不一定先得,随机,默认是不公平
常用方法:
//ReentrantLock类 void lock();//上锁 void unlock();//释放锁 boolean tryLock(long time,TimeUnit timeUnit);//尝试获取锁,成功则返回true,失败则在规定的时间内多次尝试 Condition newCondition();//获取Condition对象,这个对象有await()和signal,signallAll方法,相当于wait和notify,notifyAll //Condition类 void await();//相当于wait() void await(int timeMills);//相当于wait(int timeMills) void signal(); void signalAll();
示例:
while (true){ if (lock.tryLock(2, TimeUnit.SECONDS)){ lock.lock; try { System.out.println("我获取到锁了"); }finally { lock.unlock(); break; } } }
class TaskQueue { private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private Queue
queue = new LinkedList<>(); public void addTask(String s) { lock.lock(); try { queue.add(s); condition.signalAll(); } finally { lock.unlock(); } } public String getTask() { lock.lock(); try { while (queue.isEmpty()) { condition.await(); } return queue.remove(); } finally { lock.unlock(); } } }
==ReentrantReadWriteLock==
//读写锁,写的优先级高于读。 读写锁,多个线程可以同时获得读锁,但写锁是独占的,这样的好处是提高并发效率。适用于多个线程读,少数线程写。且没有写的情况下,才能读。这是一种悲观锁。 读锁和写锁本质上是同一个锁,只是读锁可以共享。所以在读的过程中也要加锁,防止被写占有,导致读的过程中数据变更引起脏读 //示例 public class Counter { private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); private final Lock rlock = rwlock.readLock(); private final Lock wlock = rwlock.writeLock(); private int[] counts = new int[10]; public void inc(int index) { wlock.lock(); // 加写锁 try { counts[index] += 1; } finally { wlock.unlock(); // 释放写锁 } } public int[] get() { rlock.lock(); // 加读锁 try { return Arrays.copyOf(counts, counts.length); } finally { rlock.unlock(); // 释放读锁 } } }
- ==StampledLock==
这是一种乐观锁,允许在读的时候写,性能比ReentrantReadWriteLock高
4、JUC包下的原子类
Atomic类采用的是CAS非阻塞算法,使用的是Unsafe类的compareAndSwap()作为基础。Unsafe类下还有很多其它方法,参考==《并发编程之美》==。
CAS是机器级别的指令,效率比加锁的方式高。
并发包分类:
- 其中LongBinaryOperator中的left就是每次accumulate运算后的结果,第一次计算时是构造LongAccumulator时传入的identity,right是accumulate传入的参数。
- LongAdder和LongAccumulator是相对于Atomic类而言,它们类似ThreadLocal,每个线程都有一个自己的复制变量,不需要再为了竞争原子资源而消耗时间。
- 原子类和锁的不同之处在于锁是阻塞的,只能被一个线程只有,其它线程都必须阻塞等待。而原子类不会阻塞线程,只不过同一时间只有一个线程可以操作成功。
10.5 ThreadLocal和ThreadLocalRandom
1、ThreadLocal类
- ThreadLocal
是本地线程变量,它是一个泛型类,可以存储任意类型。每个线程都有一个ThreadLocal的复制。互不影响。==但是必须先在自己的虚拟机栈里set这个值==。一般定义为==static==,最后要在==finally里清除ThreadLocal==。 - 常用方法
void set(); Object get(); void remove()清除
2、ThreadLocalRandom类
- ThreadLocalRandom是多线程下使用的Random随机数类。
ThreadLocalRandom.current()//获取当前线程的ThreadLocalRandom对象。
10.6 线程池
1、基本概念
线程池和线程组:
在main方法中自定义的线程都属于同一个线程组,同一个线程组共享变量,但是线程组使用较少。
线程池的效率高,它降低了创建线程的开销。
线程池的关系
==主要接口视图关系==:
Executor:线程池的顶级接口
ExecutorService:线程池的真正接口
ScheduledExecutorService:需要执行重复任务的线程池的接口
ThreadPoolExecutor:ExecutorService的默认实现类
ScheduledThreadPooleExecutor:继承至ThreadPoolExecturo,实现ScheduledExecutorService接口
如果要执行非重复任务,就转为ExecutorServiece,如果要执行重复任务,就转为ScheduledExecutorService。
- 工具类
==Executors==提供一些静态工厂,用于创建不同类型的线程池。
线程池的一些参数
==corePoolSize==:核心线程数量
==maximumPooleSize==:最大线程数量==keepAliveTime==:线程没有任务时,最多保存多长时间后会终止
2、线程池分类
- ==newCachedThreadPool==
//创建一个带有缓冲的线程池,缓冲的意思是可以动态调整线程池的核心线程数量,当超过了默认大小时,就扩容。可以人为指定一个区间。 ExecutorService service = Executors.newCachedThreadPool(); //线程池,无大小限制
- ==newFixedThreadPool==
//创建一个指定大小的线程池 ExecutorService service = Executors.newFixedThreadPool(2);
- ==newSingleThreadPoolExecutor==
//创建一个单线程的线程池,线程是串行执行任务 ExecutorService service = Executors.newSingleThreadExecutor() //单线程的执行顺序,先执行t1再t2,再t3,相当于固定大小为1 service.execute(t1); service.execute(t2); service.execute(t3);
==newScheduledThreadPool==
//周期性的执行任务 ExecutorService service = Executors.newScheduledThreadPool(); service.schedule(t1,2, TimeUnit.SECONDS);//延迟多久执行,只执行一次,一次性任务 service.scheduleAtFixedRate(t1,1,2,TimeUnit.SECONDS);//任务,第一次执行的时间,间隔多久执行一次,时间单位 service.scheduleWithFixedDelay(t1,1,2,TimeUnit.SECONDS);//任务,第一次执行的时间,间隔多久执行一次,时间单位 service.shutdown();//关闭线程池
scheduleAtFixedRate 间隔多久就执行一次,从任务一开始就计时,不管上一次任务是否执行完毕。但是如果该任务的执行时间大于周期,那么后续任务可能会延迟开始,但是不会并发执行 scheduleWithFixedDelay 间隔多久执行一次,指的是等上一次任务执行完毕,再间隔多久,执行
线程池的执行任务方法:
//submit Object submit(Callable
callable);//适用于有返回值的线程,虽然也可以接收Runnable,这是返回的是null,但是为了区分,最好接收Callable。 //execute void execute(Runnable runnable);//执行无返回值的线程