java多线程数据同步-java 线程池 循环同步
多线程可以提高程序性能,也属于高薪BINON的核心技术栈。 本文将全面讲解Java多线程@mikechen
下面将从六点进行详细说明:
基本概念
很多人对一些概念不是很清楚,比如同步、并发等。我们先来创建一个数据字典,避免误解。
过程
操作系统中运行的程序都是进程,比如你的QQ、播放器、游戏、IDE等。
线
一个进程可以有多个线程,比如听视频里的声音,看图片,看弹幕等等。
多线程
多线程:多个线程同时执行。
同步
Java中的同步是指通过人为的控制和调度,保证多线程对共享资源的访问变成线程安全的,从而保证结果的准确性。
例如:synchronized 关键字在保证结果准确的同时提高了性能。 线程安全优先于性能。
平行线
多个CPU实例或者多台机器同时执行一段处理逻辑,是真正意义上的同步。
并发
通过cpu调度算法,用户看似是在同时执行,但实际上从cpu运行层面来说并不是真正的同时执行。
并发往往在场景中有公共资源java多线程数据同步,所以这个公共资源往往会出现瓶颈,我们会用TPS或者QPS来体现这个系统的处理能力。
线程生命周期
在一个线程的生命周期中,它要经历五个状态:新建、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死(Dead)。
线程状态控制
具体的方法大家可以对比上面的线程状态流程图,这样具体的功能就更加清晰了:
1.开始()
启动当前线程并调用当前线程的run()方法
2. 运行()
通常需要在Thread类中重写该方法,并在该方法中声明创建的线程要执行的操作
3.收益()
释放当前CPU的执行权
4.加入()
在线程a中调用线程b的join()。 这时线程a进入阻塞状态。 线程b执行完毕后,线程a结束阻塞状态。
5.睡眠(长时间)
让线程休眠指定的毫秒数。 在指定时间内,线程被阻塞
6.等待()
一旦执行该方法,当前线程就会进入阻塞状态,一旦执行wait(),同步监视器就会被释放。
7、sleep()和wait()的异同点
相同点:一旦执行完这两个方法,两个线程都可以进入阻塞状态。
不同之处:
1)两个方法声明的位置不同:sleep()声明在Thread类中,wait()声明在Object类中
2)调用要求不同:sleep()可以在任何需要的场景下调用。 wait() 必须在同步代码块内调用。
2)关于是否释放同步监视器:如果在同步代码块的同步方法中同时使用这两种方式,sleep不会释放锁,wait会释放锁。
8.通知()
一旦这个方法被执行,一个被等待的线程就会被唤醒。 如果有多个线程正在等待,则唤醒优先级最高的线程。
9.通知所有()
一旦这个方法被执行,所有等待的线程都会被唤醒。
10.锁定支持
LockSupport.park() 和 LockSupport.unpark() 实现线程阻塞和唤醒。
创建多线程的5种方式1.继承Thread类
package com.mikechen.java.multithread;
/**
* 多线程创建:继承Thread
*
* @author mikechen
*/
class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
}
}
2.实现Runnable接口
package com.mikechen.java.multithread;
/**
* 多线程创建:实现Runnable接口
*
* @author mikechen
*/
public class MyRunnable implements Runnable {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
Thread thread = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
thread.start();
}
}
3.线程池创建
线程池:其实就是一个可以容纳多个线程的容器。 其中的线程可以重复使用,免去了频繁创建线程对象而重复创建线程过多消耗系统资源的麻烦。
package com.mikechen.java.multithread;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* 多线程创建:线程池
*
* @author mikechen
*/
public class MyThreadPool {
public static void main(String[] args) {
//创建带有5个线程的线程池
//返回的实际上是ExecutorService,而ExecutorService是Executor的子接口
Executor threadPool = Executors.newFixedThreadPool(5);
for(int i = 0 ;i < 10 ; i++) {
threadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+" is running");
}
});
}
}
}
核心参数
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
{
....
}
从上图可以看出,任务提交后,会先尝试交给核心线程池中的线程去执行,但是核心线程池中的线程数量是有限制的,所以一个缓存必须由任务队列进行。 任务缓存在队列中,然后等待线程执行。
最后由于任务太多,队列也满了。 这时候线程池中剩余的线程就会开始帮助核心线程池执行任务。
如果还是没办法正常处理新来的任务,线程池只能将新提交的任务交给饱和策略处理。
4.匿名内部类
适合创建启动线程少的环境,写起来更简单
package com.mikechen.java.multithread;
/**
* 多线程创建:匿名内部类
*
* @author mikechen
*/
public class MyThreadAnonymous {
public static void main(String[] args) {
//方式1:相当于继承了Thread类,作为子类重写run()实现
new Thread() {
public void run() {
System.out.println("匿名内部类创建线程方式1...");
};
}.start();
//方式2:实现Runnable,Runnable作为匿名内部类
new Thread(new Runnable() {
public void run() {
System.out.println("匿名内部类创建线程方式2...");
}
} ).start();
}
}
5.Lambda表达式创建
package com.mikechen.java.multithread;
/**
* 多线程创建:lambda表达式
*
* @author mikechen
*/
public class MyThreadLambda {
public static void main(String[] args) {
//匿名内部类创建多线程
new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程1");
}
}.start();
//使用Lambda表达式,实现多线程
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程2");
}).start();
//优化Lambda
new Thread(()-> System.out.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程3")).start();
}
}
线程同步
线程同步是为了防止多个线程访问一个数据对象时数据被破坏。 线程同步是保证多个线程安全访问竞争资源的一种手段。
1.普通同步方式
锁是当前实例对象,在进入同步代码前必须获取到当前实例的锁。
/**
* 用在普通方法
*/
private synchronized void synchronizedMethod() {
System.out.println("--synchronizedMethod start--");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("--synchronizedMethod end--");
}
2.静态同步方式
锁是当前类的类对象,在进入同步代码前必须获取当前类对象的锁。
/**
* 用在静态方法
*/
private synchronized static void synchronizedStaticMethod() {
System.out.println("synchronizedStaticMethod start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synchronizedStaticMethod end");
}
3.同步方法块
锁就是括号里的对象,锁住给定的对象,在进入同步代码库之前获取给定对象的锁。
/**
* 用在类
*/
private void synchronizedClass() {
synchronized (SynchronizedTest.class) {
System.out.println("synchronizedClass start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synchronizedClass end");
}
}
4.synchronized的底层实现
synchronized的底层实现完全依赖于JVM虚拟机,所以说到synchronized的底层实现,就不得不说到数据在JVM内存中的存储:Java对象头,Monitor对象监视器。
1.Java对象头
在JVM虚拟机中,对象在内存中的存储布局可以分为三个区域:
Java对象头主要包括两部分数据:
1)类型指针(Klass Pointer)
它是对象指向其类元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例;
2)标记字段(Mark Word)
用于存储对象本身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。它是一个轻量级的锁和偏向锁的钥匙。
因此,很明显,synchronized使用的锁对象是存放在Java对象头中的tag字段中的。
2.监控
监视器被描述为一个对象监视器,可以比作一个特殊的房间。 这个房间里有一些受保护的数据。 监视器保证一次只有一个线程可以进入这个房间来访问受保护的数据。 有监控器,离开房间就是释放监控器。
下图是synchronized同步代码块的反编译截图,可以清楚的看到monitor的调用。
syncrhoized加锁的同步代码块在字节码引擎中执行时,主要是通过锁对象的monitor的获取(monitorenter)和释放(monitorexit)来实现的。
多线程引入问题
多线程的优点很明显,但是多线程的缺点也很明显。 线程的使用(滥用)会给系统带来上下文切换的额外负担,线程间共享变量可能会造成死锁。
1.线程安全问题
1)原子性
并发编程中的很多操作都不是原子操作,比如:
i++; // 操作2
i = j; // 操作3
i = i + 1; // 操作4
xxxxxxxxxxbr i++; // 操作2bri = j; // 操作3bri = i + 1; // 操作4
这三个操作在单线程环境下不会出问题,但是在多线程环境下,如果不进行锁操作,很可能会出现意想不到的值 。
原子性可以通过java中的synchronized或者ReentrantLock来保证。
2)可见性
可见性:当多个线程访问同一个变量时,一个线程修改变量的值,其他线程可以立即得到修改后的值。
如上图所示,每个线程都有自己的工作内存,工作内存和主内存需要通过store和load进行交互。
为了解决多线程的可见性问题,java提供了volatile关键字。 当一个共享变量被volatile修改时,它会保证修改后的值会立即更新到主存中。 当其他线程需要读取它时,会去主存中读取新值,普通共享变量的可见性无法保证,因为变量被修改后刷新回主存的时间不确定。
2.线程死锁
线程死锁是指由于两个或多个线程持有彼此需要的资源,导致这些线程处于等待状态,无法去执行。
当线程持有彼此需要的资源后,就会互相等待对方释放资源。 如果线程不主动释放自己占用的资源java多线程数据同步,就会出现死锁,如图:
举个例子:
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
xxxxxxxxxxbr public void add(int m) {br synchronized(lockA) { // 获得lockA的锁br this.value += m;br synchronized(lockB) { // 获得lockB的锁br this.another += m;br } // 释放lockB的锁br } // 释放lockA的锁br}brbrpublic void dec(int m) {br synchronized(lockB) { // 获得lockB的锁br this.another -= m;br synchronized(lockA) { // 获得lockA的锁br this.value -= m;br } // 释放lockA的锁br } // 释放lockB的锁br}
两个线程各自持有不同的锁,然后各自尝试获取对方手中的锁,导致双方无限等待,这就是死锁。
3.上下文切换
多线程并发会更快吗? 其实不一定,因为多线程有线程创建和线程上下文切换的开销。
CPU是一种非常宝贵的资源,它的速度是非常快的。 为了保证平衡,通常会为不同的线程分配时间片。 当CPU从一个线程切换到另一个线程时,CPU需要保存当前线程的本地数据、程序指针等状态,并加载下一个要执行的线程的本地数据、程序指针等。这种切换称为上下文切换。
一般减少上下文切换的方法包括:无锁并发编程、CAS算法、使用协程等。
多线程用得好,效率可以成倍提高,但用得不好,可能会比单线程慢。
多于!
更多架构技术干货,私信【架构】查看我原创300期+BAT架构技术系列文章和1000+大厂面试题答案合集。