- 线程安全问题
- 线程安全问题的解决方案
- 数据库事务解析
- 原生线程安全的容器
- 线程池的使用
- 线程的优先级以及线程间的协调
- 守护线程的解析
- fork-join框架的使用
多线程与高并发
一、线程安全问题
- 多线程的环境 --> 将多线程并发访问改成排队机制 --> 加锁
- 多个线程操作一个资源 --> 保证多个线程操作的是多个资源 --> 每个线程操作单独的资源,大家互不影响
二、线程安全问题的解决方案
1、加锁(java对象锁)
- 自旋锁
– 还未得到锁的线程不断尝试获取锁,这种行为就是自旋(性能高,资源浪费,可能存在有线程永远获取不到锁)
– 适合同步块内的操作不是很耗时 - 互斥锁
– 还未得到锁的线程进入等待,上一个线程释放锁时它再去做获取锁的尝试。
– 适合同步块内的操作比较耗时 - 公平锁
– 各线程严格按照队列顺序去得到锁 - 非公平锁
– 各线程不严格按照队列顺序去得到锁
– synchronized是非公平锁,Lock-Condition默认是非公平锁,构造方法可切换成公平锁 - CAS–乐观锁
– java.util.concurrent.atomic包下的AtomicInteger等是基于CAS乐观锁实现的 - 悲观锁
– synchronized就是悲观锁,这种线程一旦获取锁,别的线程必须挂起
– synchronized未得到锁的线程
自旋若干次 --> 进入等待 --> 反复 --> 得到主线程释放锁的信号后再次尝试自旋若干次 --> 得到锁
非公平锁或公平锁ReentrantLock的使用示例:
/***
* @Author zzz_欢欢欢
* synchronized锁 --> 非公平锁 ---> 自适应自旋(先进行自旋,队列过长进行排队机制)
* Lock锁实现:Lock lock = new ReentrantLock(); 作为参数传入对象 lock()->锁,unlock()->解锁
*/
public class Demo1_Main {
public static void main(String[] args) {
//构造函数传入false变成公平锁
Lock lock = new ReentrantLock();
//唯一的一家店,两个客户正在购买这家店的商品
Thread th_1 = new Thread(() -> {
Demo1 demo1 = new Demo1(lock);
while (true) {
demo1.sale();
Demo_Public.sleep();
}
});
Thread th_2 = new Thread(() -> {
Demo1 demo2 = new Demo1(lock);
while (true) {
demo2.sale();
Demo_Public.sleep();
}
});
th_1.start();
th_2.start();
}
}
class Demo1 {
//可以假定为一个店铺的库存,一个人的账户余额等等,因为他是多对象公用一个值所以必须是static
private static Integer number = 100;
private Lock lock;
public Demo1(Lock lock) {
this.lock = lock;
}
//销售货物场景
public void sale() {
lock.lock();
try{
number--;
System.out.println(Thread.currentThread().getName() + " 卖出一个,当前数量为:" + number);
}finally{
lock.unlock();
}
}
}
悲观锁-非公平锁synchronized使用示例:
/***
* @Author zzz_欢欢欢
*/
public class Demo2_Main {
public static void main(String[] args){
//唯一的一家店,两个客户正在购买这家店的商品
Demo2 demo = new Demo2();
Thread th_1 = new Thread(() -> {
while (true){
demo.sale();
Demo_Public.sleep();
}
});
Thread th_2 = new Thread(() -> {
while (true){
demo.sale();
Demo_Public.sleep();
}
});
th_1.start();
th_2.start();
}
}
class Demo2{
//可以假定为一个店铺的库存,一个人的账户余额等等
//因为其是非static所以在外部多线程中如果实例化多个对象操作的不是同一个number
private Integer number = 100;
//static 变量多对象唯一,作为唯一锁 synchronized(this)如果使用this 对象在堆中不唯一就失去了锁的意义。
//synchronized(必须保证唯一)
static Object lock = new Object();
//销售货物场景
public void sale(){
synchronized (lock){
number --;
System.out.println(Thread.currentThread().getName()+" 卖出一个,当前数量为:"+number);
}
}
}
java内部自实现CAS乐观锁例子:
/***
* @Author zzz_欢欢欢
*/
public class Demo3_Main {
public static void main(String[] args) {
Demo3 demo3 = new Demo3();
Thread th_1 = new Thread(()->{
while (true){
System.out.println(Thread.currentThread().getName()+" 当前值:"+demo3.sale());
Demo_Public.sleep();
}
});
Thread th_2 = new Thread(()->{
while (true){
System.out.println(Thread.currentThread().getName()+" 当前值:"+demo3.sale());
Demo_Public.sleep();
}
});
//设置优先级-->优先也是可能性优先不一定th_1线程一定比th_2线程先执行
th_1.setPriority(10);
th_2.setPriority(1);
th_1.start();
th_2.start();
}
}
class Demo3{
//假定为库存
AtomicInteger number = new AtomicInteger(0);
public int sale(){
return number.getAndAdd(1);
}
}
2、线程封闭
- 栈封闭
– 全局变局部 gc回收处理的是堆内存,栈释放是出了方法自动释放。
– js的闭包例外,方法级别的变量不释放
例如:Struts2的C层对象是非单例,获取的变量值是全局变量进行获取,SpringMVC使用的是单例模式,通过局部变量获取前端入参。 - ThreadLocal
– 用ThreadLocal包装一个变量,那么对于不同的线程来说,线程中的变量都是独立的。
/***
* @Author zzz_欢欢欢
*/
public class Demo4_Main {
public static void main(String[] args) {
Thread th_1 = new Thread(()->{
while (true){
Integer integer = Demo.addAndReturn(Demo.threadLocal);
System.out.println(Thread.currentThread().getName()+":"+integer);
Demo_Public.sleep();
}
});
Thread th_2 = new Thread(()->{
while (true){
Integer integer = Demo.addAndReturn(Demo.threadLocal);
System.out.println(Thread.currentThread().getName()+":"+integer);
Demo_Public.sleep();
}
});
th_1.start();
th_2.start();
}
}
class Demo{
static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 100);
public static Integer addAndReturn(ThreadLocal<Integer> threadLocal){
threadLocal.set(threadLocal.get() + 100);
return threadLocal.get();
}
}
输出:
三、数据库事务解析
- 数据库的四大特性
– ACID 原子性(Atomicity),隔离性(Isolation),一致性(Consistency),持久性(Durability)
– 业务的实现以一致性为最终宗旨
– 数据库的原子性加上隔离性可以保证一致性,但需要极高的隔离级别。(不推荐) - 数据库事务的隔离级别
– Serializable (串行化):可避免脏读、不可重复读、幻读的发生。(效率极低,不推荐)
– Repeatable read (可重复读):可避免脏读、不可重复读的发生。
– Read committed (读已提交):可避免脏读的发生。(常用)
– Read uncommitted (读未提交):最低级别,任何情况都无法保证。 - 数据库乐观锁的实现
select version;
update tb set col="" and version=version+1 where id="" and version="";
- 数据库悲观锁的实现
select * from tb where a='' for update;
– 如果查询条件是唯一索引for update 会变成行级锁,否则是表级锁。
四、原生线程安全的容器
- 非并发包
– Vector
– HashTable 性能偏低 - 并发包
– CopyOnWriteArrayList 写时拷贝-读多写少的场合
– ConcurrentHashMap jdk1.8之前使用的是分段锁机制(默认16个数据段Segment<K,V>),在1.8中去除了这个限制,以锁定头结点的方式进行保证线程安全性,性能优势:对于不同节点的数据不需要争抢同一把锁,相当于Map的下标有多少就有多少的锁。
jdk1.8之前ConcurrentHashMap的put方法实现逻辑:
1) put时通过内部segmentFor(k)计算一个值,把kv放入某个Segment中
2) 外部多线程中只要不是操作一个数据段就不需要争抢同一把锁所以性能比HashTable好
五、线程池的使用
ExecutorService对象的submit()方法可以接收Runnable对象和Callable对象
execute()方法只能接收Runnable对象
–执行Runnable对象 无返回值
–执行Runnable对象 可获得返回值
如果需要调用有返回值,只能使用submit方法传入Callable对象。
无界限线程池/固定数量线程池/单线程池/计划线程池
线程的停止/挂起为什么不提倡使用 stop()/suspend()
– stop()会在结束立马释放锁
– suspend()在结束不会释放锁
– 正确的方式是加标志位,通过标志位的值来停止线程使其自然结束。
线程池类型参考博客地址:线程池种类以及具体功能说明
/***
* @Author zzz_欢欢欢
*/
public class Demo1_Main {
public static void main(String[] args) {
//无界限线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//固定数量线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
//单线程池 单任务执行场景,新任务进入排队
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
//计划线程池-->周期性任务场景
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(10);
//无返回值 execute
cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName()));
//必有返回值,但如果传入的是Runnable对象返回值为null submit
Future<Object> submit = cachedThreadPool.submit(() -> null);
}
}
class Caller implements Callable<Object> {
@Override
public Object call() throws Exception {
return null;
}
}
class Runnabler implements Runnable {
@Override
public void run() {
}
}
六、线程的优先级以及线程间的协调
示例题目:三个线程同时启动,一个线程单独打印A 一个线程B 一个线程C 如何保证正确按顺序输出ABC,不断进行循环输出。
附上源码:
step1:主方法:
package threadsPrint;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/***
* @Author zzz_欢欢欢
*/
public class MainClass {
//创建一把锁,让三个线程去抢这把锁然后进行打印
Lock lock = new ReentrantLock();
//获得每个线程的状态,唤醒线程 or 让线程继续等待
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
//设定当前打印的值 ---> 如果当前打印的是A那么下次打印只允许是B,打印完将当前值改成B
//volatile 每次改变完当前值,立马刷入主内存
volatile String currNeedPrintStr = "A";
public static void main(String[] args) {
MainClass mainClass = new MainClass();
ThreadA threadA = new ThreadA(mainClass);
ThreadB threadB = new ThreadB(mainClass);
ThreadC threadC = new ThreadC(mainClass);
threadA.start();
threadB.start();
threadC.start();
}
}
三个线程:
A线程:
/***
* @Author zzz_欢欢欢
*/
public class ThreadA extends Thread {
private MainClass mainClass;
public ThreadA(MainClass mainClass){
this.mainClass = mainClass;
}
@Override
public void run() {
while (true){
//获取锁
mainClass.lock.lock();
try {
//如果当前需要打印的标识是A的话进入
if (mainClass.currNeedPrintStr.equals("A")){
//打印A
System.out.print("A");
//将标识位改成B
mainClass.currNeedPrintStr = "B";
//将B线程唤醒
mainClass.conditionB.signal();
Thread.sleep(1000);
}
//A线程进入等待,等待被唤醒
mainClass.conditionA.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
mainClass.lock.unlock();
}
}
}
}
B线程:
/***
* @Author zzz_欢欢欢
*/
public class ThreadB extends Thread {
private MainClass mainClass;
public ThreadB(MainClass mainClass) {
this.mainClass = mainClass;
}
@Override
public void run() {
while (true) {
mainClass.lock.lock();
try {
if (mainClass.currNeedPrintStr.equals("B")) {
System.out.print("B");
mainClass.currNeedPrintStr = "C";
mainClass.conditionC.signal();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mainClass.conditionB.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
mainClass.lock.unlock();
}
}
}
}
C线程:
/***
* @Author zzz_欢欢欢
*/
public class ThreadC extends Thread {
private MainClass mainClass;
public ThreadC(MainClass mainClass) {
this.mainClass = mainClass;
}
@Override
public void run() {
while (true) {
mainClass.lock.lock();
try {
if (mainClass.currNeedPrintStr.equals("C")) {
System.out.print("C ");
mainClass.currNeedPrintStr = "A";
mainClass.conditionA.signal();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mainClass.conditionC.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
mainClass.lock.unlock();
}
}
}
}
priority()线程优先级: 数字越大优先级越高,但也无法保证百分百,只能大概进行标识哪个线程优先。
Lock-condition中的 Condition可以实现优先级准确
condition.signal()空叫(唤醒)
condition.await()等待
七、守护线程的解析
thread.setDaemon(true) 守护线程,监控其他活动线程,其他活动线程结束,守护线程没有存在的意义了也就结束了。
八、fork-join框架的使用
场景: 1+2+3+4+5+6…+100
将这个加法任务拆成10个任务,由十个线程进行执行。
1、把一个大任务,拆分成多个子任务(fork),多个线程并行执行子任务,归并结果(join)
2、海量层级的文件夹树递归
3、了解一下工作窃取模式 fork-join内部已经实现了工作窃取模式。
工作窃取模式:当十个任务执行时,如果一个线程工作效率特别快,会从别的队列里窃取任务去执行。
– ForkJoinPool对象去执行RecursiveTask对象
– 自己实现一个RecursiveTask,重写compute方法
– compute()内做分支,分支1是不可拆分的任务,直接运算出结果。分支2是可拆分任务,调用本Task类制造小任务列表。
– 最后是归并并返回结果。
代码示例:
/***
* @Author zzz_欢欢欢
*/
public class Demo_Main {
//将1+2+3+....+100拆成10个子任务进行
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> submit = pool.submit(new MyTask(1, 100));
System.out.println(submit.get());
}
}
class MyTask extends RecursiveTask<Integer> {
private Integer start;
private Integer end;
//每个线程执行任务的重量
private Integer size = 10;
public MyTask(Integer start, Integer end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= size - 1) {
return (start + end) * (end - start + 1) / 2;
} else {
List<MyTask> myTaskList = Lists.newArrayList();
for (int i = start; ; i = i + size) {
//非size的倍数在最后一次相加时进行跳出循环判断
if (end - i >=0 && end - i<=size -2){
MyTask myTask = new MyTask(i, end);
myTaskList.add(myTask);
break;
}
MyTask myTask = new MyTask(i, i + size - 1);
myTaskList.add(myTask);
//如果分组算的末数和end相等说明是最后一个Task
if (i + size - 1 == end)break;
}
invokeAll(myTaskList);
AtomicInteger sum = new AtomicInteger();
myTaskList.forEach(e -> sum.set(sum.get() + e.join()));
return sum.get();
}
}
}```