进程与线程的区别
进程:是系统进行分配和管理资源的基本单位
线程:进程的一个执行单元,是进程内调度的实体、是CPU调度和分派的基本单位,是比进程更小的独立运行的基本单位。
线程也被称为轻量级进程,线程是程序执行的最小单位。
一个程序至少一个进程,一个进程至少一个线程。
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,
这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程
的开销也比进程要小很多。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式进行。
如何处理好同步与互斥是编写多线程程序的难点。
多进程程序更健壮,进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,
而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,所以可能一个
线程出现问题,进而导致整个程序出现问题。
1、什么场景适合使用并发编程:
- 任务会阻塞线程,导致之后的代码不能执行:比如一边从文件中读取,一边进行大量计算的情况
- 任务执行时间过长,可以划分为分工明确的子任务:比如分段下载
- 任务间断性执行:日志打印
- 任务本身需要协作执行:比如生产者消费者问题
2、为什么不是开启线程越多越好-----并发编程的挑战之频繁的上下文切换
cpu为线程分配时间片,时间片非常短(毫秒级别),cpu不停的切换线程执行,在切换前会保存上一个任务的状态,
以便下次切换回这个任务时,可以再加载这个任务的状态,让我们感觉是多个程序同时运行的
上下文的频繁切换,会带来一定的性能开销
如何减少上下文切换的开销?
无锁并发编程
无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一
些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
CAS
Java的Atomic包就是使用CAS算法来更新数据,而不需要加锁。
使用最少线程。
避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这
样会造成大量线程都处于等待状态。
协程(java很少使用协程)
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
3、并发编程之死锁
public class DeadLocakDemo {
private static final Object HAIR_A =new Object();
private static final Object HAIR_B =new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized(HAIR_A){
try {
Thread.sleep(50L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (HAIR_B){
System.out.println("A成功获取B的资源");
}
}
}).start();
new Thread(()->{
synchronized(HAIR_B){
synchronized (HAIR_A){
System.out.println("B成功获A的资源");
}
}
}).start();
}
}
查看死锁
4、并发编程之线程安全
- 运行下面的程序,我们每次得到的都不是1000,而是小于1000的数。这就是并发编程线程安全的问题。为什么会出现这种情况,比如现在有一个线程获取num,执行num++,还未执行完,它的cpu时间片段到了。另一个线程开始运行访问num=0,执行num++,num=1,2,3,4它的时间cpu时间片段也到了。第一个线程继续执行num=1
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
//每次调用对num进行++操作
public static void inCreate(){
num++;
}
public static void main(String[] args) {
for(int i =0 ;i<10;i++){
new Thread(()->{
for (int j=0;j<100;j++){
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch,计数器的值减1
countDownLatch.countDown();
}).start();
}
while (true){
if(countDownLatch.getCount() == 0){
System.out.println(num);
break;
}
}
}
}
5、并发编程之资源限制
硬件资源
服务器: 1m
本机:2m
带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。
软件资源
数据库连接 500个连接 1000个线程查询 并不会因此而加快
6、线程的状态及其相互转换
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
运行(RUNNABLE):处于可运行状态的线程正在JVM中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。
阻塞(BLOCKED):线程阻塞于synchronized锁,等待获取synchronized锁的状态。
等待(WAITING):Object.wait()、join()、 LockSupport.park(),进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIME_WAITING):Object.wait(long)、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil,该状态不同于WAITING,
它可以在指定的时间内自行返回。
终止(TERMINATED):表示该线程已经执行完毕。
创建线程的方式
- 1.继承Thread,并重写父类的run方法
- 2.实现Runable接口,并实现run方法
- 3.使用匿名内部类
- 4.Lambda表达式
- 5.线程池
实际开发中,选第Runable种:java只允许单继承。接口增加程序的健壮性,代码可以共享,代码跟数据独立
public class ThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
线程的挂起跟恢复
-
什么是挂起线程?
线程的挂起操作实质上就是使线程进入“非可执行”状态下(即让线程从RUNABLE进入非RUNABLE状态),
在这个状态下CPU不会分给线程时间片,
进入这个状态可以用来暂停一个线程的运行。
在线程挂起后,可以通过重新唤醒线程来使之恢复运行 -
为什么要挂起线程?
cpu分配的时间片非常短、同时也非常珍贵。避免资源的浪费。 -
如何挂起线程?
- 被废弃的方法
thread.suspend() 该方法不会释放线程所占用的资源。如果使用该方法将某个线程挂起,
则可能会使其他等待资源的线程死锁
thread.resume() 方法本身并无问题,但是不能独立于suspend()方法存在
- 被废弃的方法
/**
* Suspend死锁
*/
public class SuspendDeadLockDemo implements Runnable{
private static Object object = new Object();
@Override
public void run() {
//Suspend持有锁,锁住object对象
synchronized (object){
System.out.println(Thread.currentThread().getName()+"占用资源");
Thread.currentThread().suspend();
}
System.out.println(Thread.currentThread().getName()+"释放资源");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SuspendDeadLockDemo(),"对比线程");
thread.start();
Thread.sleep(1000L);
thread.resume();
Thread deadThread = new Thread(new SuspendDeadLockDemo(),"死锁线程");
deadThread.start();
deadThread.resume();
}
}
这种方法为什么会出现死锁呢
这是因为程序执行的速度非常快,还没执行suspend()是就已经执行了resume()方法,后续执行到suspend()时,就没有对应的方法将其唤醒,就会一直挂起,锁定占用资源。
- 可以使用的方法
wait() 暂停执行、放弃已经获得的锁、进入等待状态
notify() 随机唤醒一个在等待锁的线程
notifyAll() 唤醒所有在等待锁的线程,自行抢占cpu资源
/**
* 锁住的是哪个对象就要调用哪个对象的wait()方法,唤醒锁住该对象,也要调用该对象的notify()方法进行唤醒
*/
public class WaitDemo implements Runnable {
private static Object waitObject = new Object();
@Override
public void run() {
synchronized (waitObject){
System.out.println(Thread.currentThread().getName()+"占用资源");
try {
waitObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"释放资源");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new WaitDemo(),"对比线程");
thread.start();
Thread thread2 = new Thread(new WaitDemo(),"对比线程2");
thread2.start();
Thread.sleep(1000L);
synchronized (waitObject){
waitObject.notify();
}
}
}
我们看到两个线程都可以进行输出,如果不释放资源,那么只会输出一个线程的占用资源,注意输出的顺序,(两个占用后释放)。与上面的一个占用一个释放,另一个线程再占用不一样
- 什么时候适合使用挂起线程?
我等的船还不来(等待某些未就绪的资源),我等的人还不明白。直到notify方法被调用
线程的中断操作
- stop() 废弃方法,开发中不要使用。
因为一调用,线程就立刻停止,剩余的程序没有执行完,此时有可能引发相应的线程安全性问题 - Thread.interrupt方法
/**
* 线程的中断
*/
public class InterruptDemo implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptDemo());
thread.start();
Thread.sleep(1000L);
thread.interrupt();
}
}
- x
- 自行定义一个标志,用来判断是否继续执行
/**
* 自行定义一个标志,用来判断是否继续执行
*/
public class MyInterruptDemo implements Runnable {
private static volatile boolean FLAG = true;
@Override
public void run() {
while (FLAG){
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyInterruptDemo());
thread.start();
Thread.sleep(1000L);
FLAG=false;
}
}
线程的优先级
-
线程的优先级告诉程序该线程的重要程度有多大。如果有大量线程都被堵塞,都在等候运行,程序会尽可能地先运行优先级的那个线程。 但是,这并不表示优先级较低的线程不会运行。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。
-
线程的优先级设置可以为1-10的任一数值,Thread类中定义了三个线程优先级,分别是:
MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10),一般情况下推荐使用这几个常量,不要自行设置数值。
不同平台,对线程的优先级的支持不同。编程的时候,不要过度依赖线程优先级,如果你的程序运行是否正确取决于你设置的优先级是否按所设置的优先级运行,那这样的程序不正确。- 任务:
快速处理:设置高的优先级
慢慢处理:设置低的优先级
- 任务:
/**
* 线程的优先级Demo
*/
public class PriorithDemo {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true){
System.out.println(Thread.currentThread().getName());
}
},"线程1");
Thread thread2 = new Thread(()->{
while (true){
System.out.println(Thread.currentThread().getName());
}
},"线程2");
thread.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
thread.start();
thread2.start();
}
}
守护线程
- 线程分类
- 用户线程、守护线程
- 守护线程:任何一个守护线程都是整个程序中所有用户线程的守护者,只要有活着的用户线程,守护线程就活着。当JVM实例中最后一个非守护线程结束时,也随JVM一起退出
- 守护线程的用处:jvm垃圾清理线程
/**
* 守护线程
*/
public class DaemonThreadDemo implements Runnable {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new DaemonThreadDemo());
//true开启守护线程,默认false。放在start()方法之前。守护线程随着用户线程的结束而结束
thread.setDaemon(true);
thread.start();
Thread.sleep(2000L);
}
}
- 建议: 尽量少使用守护线程,因其不可控
不要在守护线程里去进行读写操作、执行计算逻辑
线程安全性问题
3.1、
- 什么是线程安全性?
当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。----《并发编程实战》 - 什么是线程不安全?
多线程并发访问时,得不到正确的结果。- 运行下面的程序,我们每次得到的都不是1000,而是小于1000的数。这就是并发编程线程安全的问题。为什么会出现这种情况,比如现在有一个线程获取num,执行num++,还未执行完,它的cpu时间片段到了。另一个线程开始运行访问num=0,执行num++,num=1,2,3,4它的时间cpu时间片段也到了。第一个线程继续执行num=1
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
//每次调用对num进行++操作
public static void inCreate(){
num++;
}
public static void main(String[] args) {
for(int i =0 ;i<10;i++){
new Thread(()->{
for (int j=0;j<100;j++){
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch,计数器的值减1
countDownLatch.countDown();
}).start();
}
while (true){
if(countDownLatch.getCount() == 0){
System.out.println(num);
break;
}
}
}
}
3.2、从字节码角度剖析线程不安全操作
javac -encoding UTF-8 UnsafeThread.java 编译成.class
javap -c UnsafeThread.class 进行反编译,得到相应的字节码指令
0: getstatic #2 获取指定类的静态域,并将其押入栈顶
3: iconst_1 将int型1押入栈顶
4: iadd 将栈顶两个int型相加,将结果押入栈顶
5: putstatic #2 为指定类静态域赋值
8: return
例子中,产生线程不安全问题的原因:
num++ 不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,
多线程快递切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程安全性。
3.3、原子性操作
3.3.1、什么是原子性操作
一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:
1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。
那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。
此时A的帐户仍然有3000块,B的帐户仍然有2000块。
通俗点讲:操作要成功一起成功、要失败大家一起失败
3.3.2、 如何把非原子性操作变成原子性
volatile关键字仅仅保证可见性,并不保证原子性
synchronize关机字,使得操作具有原子性
变量添加volatile修饰
public class UnSafeThread {
private static volatile int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
//每次调用对num进行++操作
public static void inCreate(){
num++;
}
public static void main(String[] args) {
for(int i =0 ;i<10;i++){
new Thread(()->{
for (int j=0;j<100;j++){
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch,计数器的值减1
countDownLatch.countDown();
}).start();
}
while (true){
if(countDownLatch.getCount() == 0){
System.out.println(num);
break;
}
}
}
}
在inCreate()中添加synchronize关键字,那么这个方法里面的操作就具有原子性
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
//每次调用对num进行++操作
public static synchronized void inCreate(){
num++;
}
public static void main(String[] args) {
for(int i =0 ;i<10;i++){
new Thread(()->{
for (int j=0;j<100;j++){
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch,计数器的值减1
countDownLatch.countDown();
}).start();
}
while (true){
if(countDownLatch.getCount() == 0){
System.out.println(num);
break;
}
}
}
}
3.4、 深入理解synchronized
3.4.1、 内置锁
每个java对象,都可以作为实现一个实现同步的锁,这些锁称为内置锁。在java对象的对象头上是有一个锁的标志位,它通过这个标志位来确定哪个线程获得相应的锁,从而确定哪个线程能够进入这个同步代码块或者同步方法里面。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
3.4.2、 互斥锁
内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
(1)、synchronized 不能修饰类
(2)、synchronized 修饰普通方法:锁住对象的实例
public class SynDemo {
//synchronized 修饰普通方法
public synchronized void out() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
public static void main(String[] args) {
SynDemo synDemo = new SynDemo();
SynDemo synDemo2 = new SynDemo();
new Thread(()->{
try {
synDemo.out();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
synDemo2.out();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
(3)、synchronized 修饰静态方法:锁住整个类
public class SynDemo2 {
public static void main(String[] args) {
SynDemo2 synDemo = new SynDemo2();
SynDemo2 synDemo2 = new SynDemo2();
new Thread(()->{
try {
synDemo.staticOut();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
synDemo2.staticOut();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
//synchronized 修饰静态方法:锁住整个类
public static synchronized void staticOut() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}
(4)、synchronized 修饰代码块: 锁住一个对象 synchronized (lock) 即synchronized后面括号里的内容
/**
* synchronized 修饰代码块: 锁住一个对象 synchronized (lock) 即synchronized后面括号里的内容
*/
public class SynDemo3 {
private Object object = new Object();
public void myOut(){
//synchronized 修饰代码块。锁住的是 object 这个对象,不会锁住myOut()这个方法
synchronized (object){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynDemo3 synDemo = new SynDemo3();
new Thread(()->{
synDemo.myOut();
}).start();
new Thread(()->{
synDemo.myOut();
}).start();
}
}
3.5、 volatile关键字及其使用场景
能且仅能修饰变量
保证被修饰变量的可见性,volatile关键字仅仅保证可见性,并不保证原子性
禁止指令重排序
- 在多线程的情况下去操作volatile修饰的变量
A、B两个线程同时读取volatile关键字修饰的对象
A读取之后,修改了变量的值
修改后的值,对B线程来说,是可见 - 使用场景
1:作为线程开关
2:单例,修饰对象实例,禁止指令重排序
/**
* volatileDemo关键字
* 作为线程的开关
*/
public class VolatileDemo implements Runnable {
//作为线程的开关
private static volatile boolean flag = true;
@Override
public void run() {
while (flag){
System.out.println(Thread.currentThread().getName());
}
}
}
3.6、 单例与线程安全
- 饿汉式–本身线程安全
在类加载的时候,就已经进行实例化,无论之后用不用到。如果该类比较占内存,之后又没用到,就白白浪费了资源。
/**
* 在类加载的时候,就已经进行实例化,无论之后用不用到。
* 如果该类比较占内存,之后又没用到,就白白浪费了资源。
*
* 饿汉式单例是线程安全的,不需要额外的加锁操作
*/
public class HungerSingleton {
private static HungerSingleton ourInstance = new HungerSingleton();
public static HungerSingleton getInstance() {
return ourInstance;
}
//单例的任何一种写法构造方法要私有化
private HungerSingleton() {
}
public static void main(String[] args) {
for(int i = 0;i<10;i++){
new Thread(()->{
System.out.println(HungerSingleton.getInstance());
}).start();
}
}
}
- 懒汉式 – 最简单的写法是非线程安全的
在需要的时候再实例化
/**
* 懒汉式单例
* 在需要时候再实例化
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
//判断实例是否为空,为空则实例化。
if(null == lazySingleton){
lazySingleton = new LazySingleton();
}
//否则直接返回
return lazySingleton;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
//模拟实例化耗时操作,使出现不同的实例
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(LazySingleton.getInstance());
}).start();
}
}
}
如何把懒汉式变成线程安全的呢
添加synchronized 关键字,锁住整个类
public static synchronized LazySingleton getInstance(){
//判断实例是否为空,为空则实例化。
if(null == lazySingleton){
lazySingleton = new LazySingleton();
}
//否则直接返回
return lazySingleton;
}
懒汉式提高性能,双重检测锁加volatile禁止指令重排
/**
* 懒汉式单例
* 在需要时候再实例化
*/
public class LazySingleton {
//volatile禁止指令重排
private static volatile LazySingleton lazySingleton = null;
private LazySingleton(){ }
public static LazySingleton getInstance(){
//判断实例是否为空,为空则实例化。
if(null == lazySingleton){
synchronized (LazySingleton.class){
if(null == lazySingleton){
/*
*这里虽然只是new了一下,但是jvm的操作比较复杂,有可能进行指令重排
* 需要在定义单例实例的时候使用volatile关键字禁止指令重排。
*/
lazySingleton = new LazySingleton();
}
}
}
//否则直接返回
return lazySingleton;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
//模拟实例化耗时操作,使出现不同的实例
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(LazySingleton.getInstance());
}).start();
}
}
}
3.7、 如何避免线程安全性问题
3.7.1、线程安全性问题成因
1:多线程环境
2:多个线程操作同一共享资源
3:对该共享资源进行了非原子性操作
3.7.2、如何避免
打破成因中三点任意一点
1:多线程环境–将多线程改单线程(必要的代码,加锁访问)
2:多个线程操作同一共享资源–不共享资源(ThreadLocal、不共享、操作无状态化、不可变)
3:对该共享资源进行了非原子性操作-- 将非原子性操作改成原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类)
4、锁
4.1、锁的分类
自旋锁: 线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起
阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间到了)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态
重入锁: 支持线程再次进入的锁,在线程没有释放锁之前都可以再次进来。就跟我们有房间钥匙,可以多次进入房间类似
读写锁: 两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享
互斥锁: 上厕所,进门之后就把门关了,不让其他人进来
悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。version=2 version=3
公平锁:大家都老老实实排队去拿锁,对大家而言都很公平
非公平锁:一部分人排着队,但是新来的可能插队
偏向锁:偏向锁使用了一种等到竞争出现才释放锁的机制。所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
独占锁:独占锁模式下,每次只能有一个线程能持有锁
共享锁:允许多个线程同时获取锁,并发访问共享资源
4.2、深入理解Lock接口
4.2.1、Lock的使用
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
private static Lock lock = new ReentrantLock();
//每次调用对num进行++操作
public static void inCreate(){
//对需要并发保护的地方进行上锁。然后执行非原子操作
lock.lock();
num++;
lock.unlock();
}
public static void main(String[] args) {
for(int i =0 ;i<10;i++){
new Thread(()->{
for (int j=0;j<100;j++){
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch,计数器的值减1
countDownLatch.countDown();
}).start();
}
while (true){
if(countDownLatch.getCount() == 0){
System.out.println(num);
break;
}
}
}
}
4.2.2、lock与synchronized的区别
- lock
- 获取锁与释放锁的过程,都需要程序员手动的控制
- Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作
private static Lock lock = new ReentrantLock();
public static void inCreate(){
//对需要并发保护的地方进行上锁。然后执行非原子操作
lock.lock();
num++;
lock.unlock();
}
- synchronized托管给jvm执行(jdk1.5之前性能很低)
- 原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
public static synchronized void inCreate(){
num++;
}
- 实现了lock接口的锁
- 各个方法的简介
4.3、实现属于自己的锁
/**
* 自定义锁
*/
public class MyLock implements Lock {
//锁是否被持有的标志
private boolean isHoldLock = false;
//保存持有锁的线程
private Thread holdLockThread = null;
//重入的次数
private int reentryCount = 0;
/**
* 同一个时刻,能切仅能有一个线程获取到锁,其他的线程,
* 只能等待该线程释放锁之后才能获取到锁
*/
@Override
public synchronized void lock() {
if(isHoldLock && Thread.currentThread() != holdLockThread){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
holdLockThread = Thread.currentThread();
isHoldLock = true;
reentryCount++;
}
@Override
public synchronized void unlock() {
//判断当前线程是否是持有锁的线程,是重入次数减去1,不是就不能处理。
if(holdLockThread == Thread.currentThread()){
reentryCount--;
if(reentryCount == 0){
notify();
isHoldLock = false;
}
}
}
}
/**
* 验证MyLock这个锁是否可以重入
*/
public class ReentryDemo {
public Lock lock = new MyLock();
public void methodA(){
lock.lock();
System.out.println("进入方法A");
methodB();
lock.unlock();
}
public void methodB(){
lock.lock();
System.out.println("进入方法B");
lock.unlock();
}
public static void main(String[] args) {
ReentryDemo reentryDemo = new ReentryDemo();
reentryDemo.methodA();
}
}
4.4、AbstractQueuedSynchronizer浅析
AbstractQueuedSynchronizer -- 为实现依赖于先进先出 (FIFO)
等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。
此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。
子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。
假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。
应该将子类定义为非公共内部帮助器类,可用它们来实现其封闭类的同步属性。类 AbstractQueuedSynchronizer 没有实现任何同步接口。而是定义了诸如 acquireInterruptibly(int) 之类的一些方法,在适当的时候可以通过具体的锁和相关同步器来调用它们,以实现其公共方法。
此类支持默认的独占 模式和共享 模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享相同的 FIFO 队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock 中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。
此类通过支持独占模式的子类定义了一个嵌套的 AbstractQueuedSynchronizer.ConditionObject 类,可以将这个类用作 Condition 实现。isHeldExclusively() 方法将报告同步对于当前线程是否是独占的;使用当前 getState() 值调用 release(int) 方法则可以完全释放此对象;如果给定保存的状态值,那么 acquire(int) 方法可以将此对象最终恢复为它以前获取的状态。没有别的 AbstractQueuedSynchronizer 方法创建这样的条件,因此,如果无法满足此约束,则不要使用它。AbstractQueuedSynchronizer.ConditionObject 的行为当然取决于其同步器实现的语义。
此类为内部队列提供了检查、检测和监视方法,还为 condition 对象提供了类似方法。可以根据需要使用用于其同步机制的 AbstractQueuedSynchronizer 将这些方法导出到类中。
此类的序列化只存储维护状态的基础原子整数,因此已序列化的对象拥有空的线程队列。需要可序列化的典型子类将定义一个 readObject 方法,该方法在反序列化时将此对象恢复到某个已知初始状态。
tryAcquire(int)
tryRelease(int)
tryAcquireShared(int)
tryReleaseShared(int)
isHeldExclusively()
Acquire:
while (!tryAcquire(arg)) {
enqueue thread if it is not already queued;
possibly block current thread;
}
Release:
if ((arg))
unblock the first queued thread;
4.5、深入剖析ReentrantLock源码之非公平锁的实现
- 公平锁与非公平锁的区别
- 公平锁:顾名思义–公平,大家老老实实排队
- 非公平锁:只要有机会,就先尝试抢占资源
公平锁与非公平锁其实有点像在公厕上厕所。公平锁遵守排队的规则,只要前面有人在排队,那么刚进来的就老老实实排队。而非公平锁就有点流氓,只要当前茅坑没人,它就占了那个茅坑,不管后面的人排了多久。
- 非公平锁的弊端
可能导致后面排队等待的线程等不到相应的cpu资源,从而引起线程饥饿
4.6、深入剖析ReentrantLock源码之公平锁的实现
4.7、线程执行顺序之多线程debug
/**
* 多线程Debug
*/
public class ReentrantLockDebugDemo {
private int i =0;
private ReentrantLock reentrantLock = new ReentrantLock();
public void inCreate(){
reentrantLock.lock();
try {
i++;
System.out.println(i);
} finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDebugDemo reentrantLockDebugDemo = new ReentrantLockDebugDemo();
for(int j=0;j<3;j++){
new Thread(()->{
reentrantLockDebugDemo.inCreate();
}).start();
}
}
}
4.7、读写锁特性及ReentrantReadWriteLock的使用
- 特性:写写互斥、读写互斥、读读共享
- 锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
public class ReentrantReadWriteLockDemo {
private int i = 0;
private int j = 0;
private ReadWriteLock lock=new ReentrantReadWriteLock();
Lock readLock=lock.readLock();
Lock writeLock=lock.writeLock();
public void out(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"i的值===》"+i+"j的值===》"+j);
} finally {
readLock.unlock();
}
}
public void inCreate(){
writeLock.lock();
try {
i++;
Thread.sleep(500L);
j++;
} catch (Exception e) {
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockDemo reentrantReadWriteLockDemo = new ReentrantReadWriteLockDemo();
// for(int i=0;i<3;i++){
// new Thread(()->{
// reentrantReadWriteLockDemo.inCreate();
// reentrantReadWriteLockDemo.out();
// }).start();
// }
// new Thread(()->{
// reentrantReadWriteLockDemo.out();
// },"读线程").start();
// new Thread(()->{
// reentrantReadWriteLockDemo.inCreate();
// },"写线程").start();
// new Thread(()->{
// reentrantReadWriteLockDemo.inCreate();
// },"写线程").start();
// new Thread(()->{
// reentrantReadWriteLockDemo.inCreate();
// },"写线程").start();
new Thread(()->{
reentrantReadWriteLockDemo.out();
},"读线程").start();
new Thread(()->{
reentrantReadWriteLockDemo.out();
},"读线程").start();
}
}
4.8、源码之AQS如何用单一int值表示读写两种状态
4.9、锁降级
-
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
-
注意点:锁降级之后,写锁并不会直接降级成读锁,不会随着读锁的释放而释放,因此需要显式地释放写锁
-
是否有锁升级?
在ReentrantReadWriteLock里面,不存在锁升级这一说法 -
锁降级的应用场景
用于对数据比较敏感,需要在对数据修改之后,获取到修改后的值,并进行接下来的其他操作
/**
* 锁降级演示
*/
public class LockDegradeDemo {
private int i =0;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
public void doSomething(){
writeLock.lock();
//模拟其他复杂业务操作
try {
i++;
//在写锁还没有释放之前,去获取读锁
readLock.lock();
} finally {
writeLock.unlock();
}
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
if( i == 1){
System.out.println("i的值是=====》1");
} else {
System.out.println("i的值是"+i);
}
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
LockDegradeDemo lockDegradeDemo = new LockDegradeDemo();
for(int i=0;i<4;i++){
new Thread(()->{
lockDegradeDemo.doSomething();
}).start();
}
}
}
4.10、StampedLock原理及使用
1.8之前,锁已经那么多了,为什么还要有StampedLock?
一般应用,都是读多写少,ReentrantReadWriteLock 因读写互斥,故读时阻塞写,因而性能上不去。可能会使写线程饥饿
-
StampedLock的特点
- 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
- 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
- StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
- 支持锁升级跟锁降级
- 可以乐观读也可以悲观读
- 使用有限次自旋,增加锁获得的几率,避免上下文切换带来的开销
- 乐观读不阻塞写操作,悲观读,阻塞写得操作
-
StampedLock的优点
相比于ReentrantReadWriteLock,吞吐量大幅提升 -
StampedLock的缺点
- api相对复杂,容易用错
- 内部实现相比于ReentrantReadWriteLock复杂得多
-
StampedLock的原理
- 每次获取锁的时候,都会返回一个邮戳(stamp),相当于mysql里的version字段
- 释放锁的时候,再根据之前的获得的邮戳,去进行锁释放
-
使用stampedLock注意点
如果使用乐观读,一定要判断返回的邮戳是否是一开始获得到的,如果不是,要去获取悲观读锁,再次去读取
/**
* StampedLock
*/
public class StampedLockDemo {
//成员变量
private double x,y;
//锁实例
private final StampedLock sl = new StampedLock();
//排它锁-写锁(writeLock)
void move(double deltax,double deltaY){
long stamp = sl.writeLock();
try {
x += deltax;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
//乐观读锁
double distanceFromOrigin(){
//尝试获取乐观读锁(1)
long stamp = sl.tryOptimisticRead();
//将全部变量拷贝到方法体栈内(2)
double currentX = x, currentY = y;
//检查在(1)获取到读锁票据后,锁有没有被其他写线程排他性抢占(3)
if(!sl.validate(stamp)){
//如果被抢占则获取一个共享读锁(悲观获取)(4)
stamp = sl.readLock();
try {
//将全部变量拷贝到方法体占内(5)
currentX = x;
currentY = y;
} finally {
//释放共享读锁(6)
sl.unlockRead(stamp);
}
}
//返回计算结果(7)
return Math.sqrt(currentX * currentX + currentY * currentY);
}
//使用悲观锁获取读锁,并尝试转换为写锁
void moveIfAtOrigin(double newX ,double newY){
//这里可以使用乐观读锁替换(1)
long stamp = sl.readLock();
try {
//如果当前点在原点则移动(2)
while (x == 0.0 && y == 0.0){
//尝试将获取的读锁升级为写锁(3)
long ws = sl.tryConvertToWriteLock(stamp);
//升级成功,则更新票据,并设置坐标值,然后退出循环(4)
if(ws != 0L){
stamp = ws;
x = newX;
y = newY;
break;
} else {
//读取升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试(5)
sl.unlockRead(stamp);
//再试尝试获取writeLock()获取stamp
stamp = sl.writeLock();
}
}
} finally {
//释放锁(6)
sl.unlock(stamp);
}
}
}