1.多线程基础
其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求作出响应
另外,由于同一个进程的所有线程是共享同一内存,所以不需要特殊的数据传输机制,对于任务的协调操作、资源分配,能做的更好
创建多线程的方式
-
Thread
继承Thread类。重写run方法。启动时,调用实例的start方法 -
Runnable
实现Runnable接口。实现run方法。启动时,调用实例的start方法 -
匿名内部类
实际上所有多线程都是通过运行Thread的start()方法来运行的。
实现Runnable接口的优势
5. 适合多个相同的程序代码的线程去共享同一个资源
6. 可以避免java中的单继承的局限性
7. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和数据独立
8. 线程池只能放入实现Runnable或者Callable类线程,不能直接放入Thread
同步方法
- 代码上加synchronized(lock)
- 方法上加synchronized关键字
- java并发包的ReentrantLock对象(可重入锁)
ReentrantLock lock = new ReentrantLock()
lock.lock();
// dosomething
lock.unlock();
线程状态
NEW(新建)
线程刚被创建,但是并未启动
RUNNABLE(可运行)
可运行状态,可能在运行,也可能没有,取决于操作系统的处理
BLOCKED(锁阻塞)
当一个线程尝试获取一个对象锁,而锁被其他线程持有时,进入阻塞状态。直到获取到锁,进入RUNNABLE状态
WAITING(无线等待)
一个线程等待另一个线程执行(唤醒)动作时,该线程进入WAITING状态,必须等待其他线程调用notify或者notifyAll才能唤醒
TIMED_WAITING(计时等待)
同WAITING状态。不过等时间到了会自动唤醒
TERMINATED(被终止)
因为run方法正常退出而死亡,或者因为没有捕获的异常而终止了run方法而死亡
wait 与 sleep
- sleep方法是属于Thread的;而wait是属于对象的
- sleep时,会继续持有锁;而wait是用锁来调用的,会释放锁
线程终止
结束线程有以下三种方法:
- 设置退出标志,使线程正常退出(让run方法结束)
- 使用interrupt()方法终端线程
- 使用stop方法强行终止线程(不推荐)
join 与 yield
join()方法会将其他线程插入到当前线程,将并行变为顺序
yield()方法,让当前线程回到可运行状态,将cpu让给其他线程(基本没啥用)
2.多线程并发的三个特性
-
原子性
要保证操作原子性,才能保证不出现一些意外的问题 -
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立刻看到修改后的值。 -
有序性
程序执行的顺序按照代码的先后顺序执行。虚拟机可能会对代码进行重排序。但是在单线程情况下,不会影响运行结果
如果有一个没做到,就可能会导致错误的结果
3.Java内存可见性
Java内存模型
JMM(Java Memory Model)
每个线程都有自己的内存(可以用-Xss控制大小),JMM线程操作内存的基本的规则:
- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不得在主内存中读写
- 不同线程之间无法直接访问其他线程本地内存中的变量,线程间变量的传递需要通过主内存来完成
可见性介绍
可见性:一个线程对共享变量值的修改,能够及时被其他线程看到
共享变量:如果一个变量在多个线程的本地内存中都存在副本,那么这个变量就是共享变量
JMM通过让线程访问主内存的共享变量,来解决数据不可见的问题
4.synchronized
synchronized可以保证方法或代码在执行时,同一时刻只有一个线程执行。并且可以保证内存可见性。
解决可见性问题
JMM关于synchronized的两条规定
- 线程解锁前(退出同步时),必须把自己工作内存中共享变量的最新值刷新到主内存
- 线程加锁时(进入同步时),会清空工作内存中共享变量的值,从而通过主内存获取共享变量的值
锁优化
-
自旋锁
在等待锁的时候,做一段无意义的自旋,等待锁释放。如果没有释放,就进入阻塞状态 -
锁消除
如果程序能检测到共享变量没有加锁的必要,就会去掉锁 -
锁粗化
将没必要的多次加锁解锁合成一个 -
偏向锁
检测是否为偏向锁、锁标识以及ThreadId,减少不必要的CAS操作 -
轻量级锁
使用CAS原子指令,主要用于锁竞争不激烈的情况 -
重量锁
操作系统底层的锁机制。