目录
什么是线程?
jave创建线程方式有几种?
线程中常用的方法
线程状态
多线程
解决线程安全问题
线程通信
何为并发编程?
并发执行和并行执行
线程的三个主要问题:
1、不可见性:
2、乱序性:
3、非原子性:
总结:
volatile关键字
编辑
如何保证原子性
锁
原子变量
原子类
CAS 比较并交换
Java中锁的分类
乐观锁/悲观锁
可重入锁
读写锁
共享锁/独占锁
分段锁
自旋锁
公平锁/不公平锁
偏向搜,轻量级锁,重量级锁
无锁状态:
偏向锁状态:
轻量级锁状态:
重量级锁状态:
对象结构
synchronized锁实现
AQS
实现原理
ReentrantLock锁实现
构造方法:
正常获得锁的方法
什么是线程?
进程是操作系统分配资源的最小的单位
线程是cpu执行的最小单元
线程是进程中一个独立的任务,一个进程中可以有多个线程;
jave创建线程方式有几种?
继承 Thread类
实现Runnable接口
实现Callable接口
线程中常用的方法
run() 线程要执行 的任务写在run()
start() 启动线程
sleep() 线程休眠指定时间
join() 等待线程结束
yiled() 线程让步 主动让出cpu执行权
线程状态
new Thread() 新建状态
start() 就绪状态
获得了cpu执行权 运行状态
sleep wait 输入 等待获取锁 阻塞状态
任务执行完了,出现异常 死亡/销毁
多线程
一个进程中可以创建多个线程,执行多个任务,提高程序运行效率
尝试写一个多线程程序,去分解读取一个较大的文件
package com.ffyc.javaPro.thread;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
public class MultiThreadFile {
public static void main(String[] args) throws Exception {
String sourseFilePath = "D:/fg.exe";//源文件地址
String targerFilePath = "D:/fg1.exe";//目标文件地址
long length = new File(sourseFilePath).length();//计算文件大小
MultiThreadFile multiThreadFile = new MultiThreadFile();
//启动
multiThreadFile.startThread(5, length, sourseFilePath, targerFilePath);
}
/**
*
* @param threadnum 要创建的线程数量
* @param fileLength 文件大小
* @param sourseFilePath 源文件地址
* @param targerFilePath 目标文件地址
*/
public void startThread(int threadnum, long fileLength,String sourseFilePath, String targerFilePath) {
long modLength = fileLength % threadnum;//计算文件是否可以被整除
long avgLength = fileLength / threadnum;//计算每个线程平均读取的字节数量
System.out.println(avgLength);
//循环
for (int i = 0; i < threadnum; i++) {
System.out.println((avgLength * i) + "-----" + (avgLength * (i + 1)));
//启动线程
new FileWriteThread((avgLength * i), (avgLength * (i + 1)),sourseFilePath, targerFilePath).start();
}
//不能整除,再创建线程读取最后剩余内容
if (modLength != 0) {
System.out.println(modLength);
new FileWriteThread((avgLength * 4), modLength, sourseFilePath,targerFilePath).start();
}
}
class FileWriteThread extends Thread {
private long begin;
private long end;
private RandomAccessFile soursefile; //源文件对象
private RandomAccessFile targerFile;//目标文件对象
/**
*
* @param begin 开始位置
* @param end 结束位置
* @param sourseFilePath 源文件
* @param targerFilePath 目标文件
*/
public FileWriteThread(long begin, long end, String sourseFilePath,String targerFilePath) {
this.begin = begin;
this.end = end;
try {
this.soursefile = new RandomAccessFile(sourseFilePath, "rw");
this.targerFile = new RandomAccessFile(targerFilePath, "rw");
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
public void run() {
try {
soursefile.seek(begin);//指定开始读的位置
targerFile.seek(begin);
int hasRead = 0;//记录每次实际读取的字节数量
byte[] buffer = new byte[1024];
while (begin < end && (hasRead = soursefile.read(buffer))!=-1) {
begin += hasRead;
targerFile.write(buffer, 0, hasRead);//
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
soursefile.close();
targerFile.close();
} catch (Exception e) {
}
}
}
}
}
多个线程访问同一个共享数据,线程安全问题
解决线程安全问题
加锁
synchronize 关键字
修饰代码块
synchronize(同步对象){}
修饰方法
静态方法 同步对象是类的class对象
非静态方法 同步对象是this
synchronize 关键字 实现就靠底层指令实现,既可以修饰代码块,也可以修饰方法 隐式加锁和释放锁
ReentrantLock 类
ReentrantLock 是类 靠java代码控制,只能修饰代码块,手动加锁和释放锁
线程通信
wait() 线程等待
notify() 唤醒等待的线程(优先级高的)
notifyALL() 唤醒所有等待的线程
wait()和sleep()区别
wait()是Object类中方法 等待后需要别的线程唤醒 等待后可以释放锁
sleep()是Thread类中的方法 休眠时间到了以后,可以自动唤醒 不会释放锁
何为并发编程?
前提是多线程场景。
多线程优点:在一个进程中可以有多个线程,同时执行不同的任务,提高程序响应速度,提高cpu利用率,同时压榨硬件的剩余价值。
缺点:多个线程同时访问共享的数据。 卖票,抢购,秒杀...... 用户同时向后端发送请求
好多请求同时访问数据会出现问题,并发编程,就是要让这些同时到来的请求,并发的执行(在一个时间段内,一个一个依次执行)
并发执行和并行执行
并发执行:既有同时的意思,但是在计算机领域中,又有依次交替执行的意思
并行执行:在一个时间节点上,真正的同时进行
线程的三个主要问题:
1、不可见性:
JMM java 内存模型。
由于Java内存模型分为主内存和工作内存(缓存区),这一设计思想也是来源于cpu高速缓存的。
所有的变量都存储在主内存中,当线程要对主内存中的数据操作时,首先要将主内存中的数据加载到线程的工作内存中才能进行操作。
这样的话就会产生不可见性。两个线程同时操作一个数据时,其中一个线程在自己的工作内容中修改了数据,而且两个线程是不知道的。
package com.ffyc.javaPro.thread;
public class ThreadDemo implements Runnable{
private boolean flag = false;//共享数据
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.flag = true;//让一个线程修改共享变量值
System.out.println("ThreadDemo:"+this.flag);
}
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
package com.ffyc.javaPro.thread;
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
Thread t = new Thread(td);//创建线程
t.start();
//main线程中也需要使用flag变量
while(true){
if(td.getFlag()){
System.out.println("main---------------");
break;
}
}
}
}
线程t修改了flag的值, 但是main线程仍然拿到的是刚开始读取到的false
2、乱序性:
为了进一步的优化,在cpu执行一些指令时,有的指令需要等待数据的加载;此时会先将后面的某些执行提前执行,这样指令的执行顺序就会打乱。
这种情况就会出现乱序性。(乱序执行时,也是有基本的原则的,两条直接有关系的语句不能打乱执行的)
int a = 10; int b = 10; int c = a+b;最后的int c = a+b;不能放在a,b声明前。
package com.ffyc.javaPro.thread;
/*
模拟指令重排序
*/
public class Reorder {
private static int x;
private static int y;
private static int a;
private static int b;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
3、非原子性:
线程的切换执行会带来非原子性问题‘
cpu的执行在指令的层面是原子性的,但是高级语言一条语句往往需要被编译成多条指令执行,这样多线程场景下,切换执行时,也会造成指令的执行不是原子性的
package com.ffyc.javaPro.thread;
import java.util.concurrent.atomic.AtomicInteger;
public class Atomic {
private static int num=0;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// num = 0 num+1 num=num+1 1 1
System.out.println("线程"+(num++));
}
}.start();
}
}
}
总结:
Java内存模型的缓存区导致了不可见性;
编译器优化导致了乱序性;
线程的切换执行导致了指令执行的非原子性。
volatile关键字
在t线程的共享数据前添加volatile关键字
1、volatile 修饰的变量,在一个线程中被修改后,对其它线程立即可见
package com.ffyc.javaPro.thread;
public class ThreadDemo implements Runnable{
private volatile boolean flag = false;//共享数据
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.flag = true;//让一个线程修改共享变量值
System.out.println("ThreadDemo:"+this.flag);
}
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
2、volatile禁止cpu对指令重排序
package com.ffyc.javaPro.thread;
/*
模拟指令重排序
*/
public class Reorder {
private volatile static int x;
private volatile static int y;
private volatile static int a;
private volatile static int b;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
volatile关键字,就可以解决不可见性和乱序性,但是不可以解决非原子性
非原子性只能通过加锁的方式解决
synchronize,ReentrantLock
如何保证原子性
“同一时刻只有一个线程执行”我们称之为互斥。如果我们能够保证对共享 变量的修改是互斥的那么就都能保证原子性了。
锁
锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的 一种实现。
synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意! synchronized 并不能改变 CPU 时间片切换的特点,只是当其他线程要访问这个 资源时,发现锁还未释放,所以只能在外面等待。
synchronized 一定能保证原子性,因为被 synchronized 修饰某段代码 后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一 定能保证原子操作。
原子变量
如果你粗略的看一下 JUC(java.util.concurrent 包),那么你可以很显眼的 发现它俩:
一个是 locks 包,一个是 atomic 包,它们可以解决原子性问题。
加锁是一种阻塞式方式实现
原子变量是非阻塞式方式实现
原子类
Java中++操作在多线程情况下就是非原子性,想要让其实现原子性操作,必须对其进行加锁。
Java中对于++操作,除了加锁,还提供了另外一种实现原子操作的方案。
使用Java中提供的一些原子类来实现
package com.ffyc.javaPro.thread;
import java.util.concurrent.atomic.AtomicInteger;
public class Atomic {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(atomicInteger.incrementAndGet());
}
}.start();
}
}
}
CAS 比较并交换
是一种无锁实现(不加锁);
是如何让不加锁,在多线程场景下实现变量的原子性操作;
启用自旋思想实现,
AtomicInteger
incrementAndGet() 不加锁实现++操作
采用cas思想(不加锁,自旋)
当线程a第一次操作时,先从主内存将其数据加载到工作内存,可以把这次拿到的值称为预期值,在工作内存中对其进行改变,将新值写回主内存时,再次比较主内存中的值马,与拿到的预期比较。
如果第一次拿到的值与最新的主内存中的值相同,说明没有其他线程修改,直接将线程A更新后的值写回主内存;
如果第一次拿到的值与最新的主内存中的值不相同,说明其他线程已经修改过了,那么线程A的值就作废了,需要从主内存中读取值,再次重复之前的操作。
这种做法适合于线程数量少,由于不加锁,线程都不会阻塞,所以线程一直尝试对变量进行修改的操作,效率高于加锁;
但是线程数量如果比较多,所有线程一直自旋,尝试操作,会导致cpu工作量很大了。
还有可能出现ABA问题:
package com.ffyc.javaPro.thread;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicABA {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(100);//默认值为100
new Thread(() -> {
System.out.println(atomicInteger.compareAndSet(100, 101));//设置预期值是100 修改值为101
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(101, 100));;//设置预期值是101 修改值为100
System.out.println(atomicInteger.get());
}).start();
Thread.sleep(1000);
new Thread(() -> {
System.out.println(atomicInteger.compareAndSet(100, 101));//返回true 说明修改成功 发生ABA问题
System.out.println(atomicInteger.get());
}).start();
}
}
可以使用带版本号的原子类
package com.ffyc.javaPro.thread;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicABA1 {
public static void main(String[] args) throws InterruptedException {
AtomicStampedReference stampedReference = new AtomicStampedReference(100, 0);
new Thread(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
stampedReference.compareAndSet(101,100, stampedReference.getStamp(),stampedReference.getStamp() + 1);//2
}).start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = stampedReference.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(String.format(" >>> 修改 stampedReference :: %s ", result));
}).start();
}
}
Java中锁的分类
乐观锁/悲观锁
悲观锁:认为在多线程操作时,认为不加锁会出现问题的,是一种加锁的实现,
synchronize,ReentrantLock都属于锁,称为悲观锁
乐观锁:认为在多线程操作时,不加锁也是不会出现问题的,例如原子类,采用不加锁,自旋的 方式实现
可重入锁
可重入锁又名递归锁,当一个线程进入外层方法获得锁时,仍然可以进入到内层方法,而且内层方法和外层方法使用的是同一把锁。
如果不可重入,就会导致进入不了内存方法,导致死锁
读写锁
ReentrantReadWriteLock
实现读写锁
有读锁
也有写锁
读读不互斥 多个线程进入读锁,此时没有线程进入写锁,那么就是多个线程同时进入到读锁区域
一旦有操作进行,那么读操作就不能进入
读写互斥
写写互斥
共享锁/独占锁
读写锁中的 读锁是共享的,在没有写的情况下,可以有多个进程同时进入到读锁代码块中
读写锁中的 写锁,synchronize,ReentrantLock都属于独占锁,一次只允许一个线程进入到代码块中去
分段锁
分段锁也不是一种实际的锁,是一种实现思想,将锁的粒度细化,提高效率
例如ConcurrentHashMap的实现
由于ConcurrentHashMap底层哈希表有16个空间,可以用每一个位置上的第一个节点当作锁,可以同时由不同的线程操作不同的位置,只是同一位置多个线程不能同时操作。
自旋锁
自旋锁也不是一种实际的锁,是通过不断的自旋重试的方式进行操作的,在低并发的场景下效率较高
公平锁/不公平锁
非公平锁:就是不分先来后到,谁先抢到,谁先执行 synchronized就是非公平锁
公平锁:可以做到按照请求顺序分配锁,可以进行排队,ReentrantLock底层有两种实现,默认是非公平的,也可以通过AQS队列实现公平(排队)。
偏向搜,轻量级锁,重量级锁
synchronized锁实现时,在同步对象中可以记录锁的状态:
无锁状态:
没有任何线程获取锁;
偏向锁状态:
只有一个线程一直来获取锁,此时会在对象头中记录进程的id,id相同也可以获取锁,锁状态为偏向锁
轻量级锁状态:
当锁状态为偏向锁时,继续有其他进程过来获取锁,锁状态升级为轻量级锁,线程不会进入阻塞状态,一直自旋获得锁
重量级锁状态:
当锁状态为轻量级锁时,线程数量持续增多,且线程自旋次数达到一定数量时,锁状态升级为重量级锁,线程进入到阻塞状态,等待操作系统调度执行
不同的锁状态也是Java对synchronized锁进行的优化
对象结构
在 Hotspot 虚拟机中,对象在内存中的布局分为三块区域:对象头、实例 数据和对齐填充;Java 对象头是实现 synchronized 的锁对象的基础,一般而言, synchronized 使用的锁对象是存储在 Java 对象头里。它是轻量级锁和偏向锁的 关键。
对象头中有一块区域称为 Mark Word,用于存储对象自身的运行时数据,如哈 希(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等等。
32 位操作系统 Mark Word 为 32bit 为,64 位操作系统 Mark Word 为 64bit. 下面就是对象头的一些信息:
Java 代码打印对象头信息
添加依赖
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
打印出相关的对象头信息
System.out.println(ClassLayout.parseInstance(myClass).toPrintable());
尝试加锁改变对象头信息
synchronized (myClass){
System.out.println(ClassLayout.parseInstance(myClass).toPrintable());
}
package com.ffyc.javaPro.thread;
public class MyClass {
public static void main(String[] args) {
MyClass myClass = new MyClass();
System.out.println(ClassLayout.parseInstance(myClass).toPrintable());
}
}
package com.ffyc.javaPro.thread;
public class MyClass {
public static void main(String[] args) {
MyClass myClass = new MyClass();
System.out.println(ClassLayout.parseInstance(myClass).toPrintable());
synchronized (myClass){
System.out.println(ClassLayout.parseInstance(myClass).toPrintable());
}
}
}
synchronized锁实现
synchronized锁实现是隐式的,可以修饰方法,也可以修饰修饰代码块
底层实现是需要依赖字节码指令的
修饰方法时,会在方法上添加一个ACC_SYNCHRONIZED标志,依赖底层的监视器实现锁的控制
修饰代码块时,为同步代码块添加monitorenter指令,进行监视,执行结束会执行monitorexit指令
有线程进入,计数器+1,线程结束时,计数器-1
AQS
AQS的全称为( AbstractQueuedSynchronizer ),这个类在 java.util.concurrent.locks包下面
抽象同步队列,是并发编程中许多实现类的基础,例如Reentrant Lock底层就是用到了AQS
实现原理
AQS类中维护了一个状态 private volatile int state;
还维护了一个内部类
static final class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;//存储等待的线程
}
维护了一个双向链表
private transient volatile Node head;
private transient volatile Node tail;
ReentrantLock锁实现
类结构
ReentrantLock 总共有三个内部类,并且三个内部类是紧密相关的.
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
ReentrantLock lock = new ReentrantLock();
public void test(){
lock.lock();//也是一直自旋的
System.out.println("同步代码块");
lock.unlock();
}
public static void main(String[] args) {
new ReentrantLockDemo().test();
}
}
构造方法:
ReentrantLock底层有两种实现方式:
1、默认的 是非公平实现
NonfairSync 类继承了 Sync 类,表示采用非公平策略获取锁,其实现 了 Sync 类中抽象的 lock 方法.
static final class NonfairSync extends Sync {
//加锁
final void lock() {
//若通过 CAS 设置变量 state 成功,就是获取锁成功,则将当前线程设置为独占线程。
//若通过 CAS 设置变量 state 失败,就是获取锁失败,则进入 acquire 方法进行后续处理。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);//正常流程获取锁
}
}
2、公平实现
FairSync 类也继承了 Sync 类,表示采用公平策略获取锁,其实现了 Sync 类中 的抽象 lock 方法.
static final class FairSync extends Sync {
final void lock() {
acquire(1);//正常流程获取锁
}
}
正常获得锁的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}