JAVA如何开启线程?怎么保证线程安全?
线程和进程的区别:进程是操作系统进行资源分配的最小单元。线程是操作系统进行任务分配的最小单元,线程隶属于进程。
如何开启线程? 1、继承Thread类,重写run方法。2、实现Runnable接口,实现run方法。3、实现Callable接口,实现call方法。通过FutureTask创建一个线程,获取到线程执行的返回值,4、通过线程池来开启线程。
怎么保证线程安全? 加锁: 1、JVM提供的锁,也就是Synchronized关键字。2、JDK提供的各种锁 Lock.
而大多数情况下JAVA中如果涉及到多线程执行会使用线程池。
Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?
1、Synchronized关键字,用来加锁。Volatile只是保持变量的线程可见性。通常适用于一个线程写,多个线程读的场景。
2、不能。Volatile关键字只能保证线程可见性,不能保证原子性。
3、Volatile防止指令重排。整笑了,synchronized也可以防止代码重排。
聊聊单例模式关于线程的处理模型
下方这种方式可以保证多线程时线程安全,但是无论对象是否使用都会new
线程不安全
锁过重
会存在重复创建的问题。
最佳效果
volatile实例:
分析一下上面的代码,主线程开启一个匿名线程对象在flag=TRUE的情况一直运行,然后主线程睡眠100毫秒后将flag修改为FALSE。简单分析之后我们可以推出结论,因为static是静态的,匿名线程对象会被停止,但是事实上没有停止这需要了解一下JMM模型,如下
Java内存模型:JMM允许线程在本地缓存中保存共享变量的副本。这意味着,即使一个线程修改了static变量的值,这个修改可能不会立即反映到其他线程的本地缓存中,从而导致不可见性。
这时我们明白了即使是静态变量,每一个线程也都会自己存一个副本,所以main线程修改的实际上是自己线程内存里面的值。
当我们将变量使用volatile修饰以后,就可以保证其他线程在修改时会立刻同步。当然使用更严谨的说法是会强制让所有的线程公共内存中取值。
除此之外还有一种方法可以可以做到就是synchronized方法。先看一个案例。
package com.fds.demo.thread.test;
/**
* @Author fandongsheng
* @Date 2024/6/23 12:01
*/
public class Service {
private boolean isRun = true;
public void runMethod() {
while (isRun) {
//System.out.println(isRun);
}
System.out.println("last:" + isRun);
System.out.println("结束了");
}
public void stopRun() {
isRun = false;
}
public boolean isRun() {
return isRun;
}
}
package com.fds.demo.thread.test;
/**
* @Author fandongsheng
* @Date 2024/6/23 12:02
*/
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.runMethod();
}
}
package com.fds.demo.thread.test;
/**
* @Author fandongsheng
* @Date 2024/6/23 12:02
*/
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
service.stopRun();
}
}
package com.fds.demo.thread.test;
/**
* @Author fandongsheng
* @Date 2024/6/23 12:04
*/
public class Main {
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
ThreadA threadA = new ThreadA(service);
ThreadB threadB = new ThreadB(service);
threadA.start();
Thread.sleep(1000);
threadB.start();
Thread.sleep(1000);
System.out.println(service.isRun());
}
}
实验结果
通过实验上面的代码可以得出结论,B线程修改了标记但是线程A明显不知道还在运行。
修改上面的Service类,再次运行发现停止了。
package com.fds.demo.thread.test;
/**
* @Author fandongsheng
* @Date 2024/6/23 12:01
*/
public class Service {
private boolean isRun = true;
public void runMethod() {
while (isRun) {
String lock = new String();
synchronized (lock){
}
//System.out.println(isRun);
}
System.out.println("last:" + isRun);
System.out.println("结束了");
}
public void stopRun() {
isRun = false;
}
public boolean isRun() {
return isRun;
}
}
synchronized关键字会把私有内存中的数据同公共内存同步。
除了使用volatile关键字,线程A会主动同步主内存中的变量的情况通常发生在以下几种场景中:
- 使用synchronized关键字:
-
- 当一个线程(如线程A)访问一个被synchronized修饰的代码块或方法时,它首先会尝试获取锁。
- 如果成功获取锁,则进入临界区(critical section),此时线程A对共享变量的修改会在临界区内完成。
- 当线程A退出临界区(即释放锁)时,JVM会确保所有对共享变量的修改都被刷新到主内存中。
- 其他线程(如线程B)在获取到同一个锁并进入临界区时,会从主内存中读取最新的变量值。
- 使用Lock接口:
-
- java.util.concurrent.locks包中的Lock接口提供了更为复杂的锁控制。
- 线程A在调用lock()方法获取锁后,对共享变量的修改会在持有锁期间进行。
- 当线程A调用unlock()方法释放锁时,JVM同样会确保所有修改都被同步到主内存中。
- 写入操作:
-
- 在JVM的内存模型中,写入操作(store和write)是确保数据从线程的工作内存传送到主内存的关键步骤。
- 即使不使用volatile或同步机制,线程A在修改变量后,JVM也会在适当的时候(如线程结束、锁释放等)将修改同步到主内存。
- 但是,这种同步是隐式的、不可控的,并且不保证立即发生。
- JVM的内存管理:
-
- JVM的垃圾回收器(Garbage Collector, GC)在回收对象时,可能会触发对象引用的清理和写回主内存的操作。
- 如果线程A持有一个即将被回收对象的引用,并在其工作内存中进行了修改,那么GC在回收该对象时可能会将修改同步到主内存。
- 但是,这种情况通常与正常的程序逻辑无关,且不可控。
- 在上面的代码中循环打印,可能导致缓存区过多,引发清楚也会导致主动同步。
- 线程间的显式通信:
-
- 通过使用wait()和notify()/notifyAll()等线程间通信方法,线程A可以在满足某种条件时显式地将修改同步到主内存,并通知其他等待的线程。
- 这种情况下,线程A的修改会在调用notify()/notifyAll()方法时同步到主内存,并唤醒等待的线程。
总结来说,除了使用volatile关键字外,线程A会主动同步主内存中的变量主要通过synchronized关键字、Lock接口、写入操作、JVM的内存管理以及线程间的显式通信等方式实现。这些机制确保了线程间对共享变量的修改能够被正确地同步和传播。
接下来聊聊代码重排的问题
分析一下下面的代码,线程一和线程二中执行赋值操作两个线程是并发的,而且两个线程赋值可能是交错的,但是无论怎么交错,a和b的值都不可能同时为0,因为线程一第一件事就修改a的值,线程二第一件事是修改b的值。
package com.fds.demo.thread.test0;
/**
* @Author fandongsheng
* @Date 2024/6/23 12:35
*/
public class Main {
private static int x = 0;
private static int y = 0;
private static int a = 0;
private static int b = 0;
private static int c = 0;
private static int d = 0;
private static int e = 0;
private static int f = 0;
private static long count = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
x = 0;
y = 0;
a = 0;
b = 0;
c = 0;
d = 0;
e = 0;
f = 0;
count++;
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
c = 101;
d = 102;
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
e = 101;
f = 102;
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
String info = "count " + count + " x" + x + " y" + y;
if (x == 0 && y == 0) {
System.out.println("分支1");
System.err.println(info);
break;
} else if (x==1&&y==1){
System.out.println("分支2");
System.out.println(info);
}else if (x==1 &&y==0){
System.out.println("分支3");
System.out.println(info);
}else{
System.out.println("分支4");
System.out.println(info);
}
}
}
}
观察实验结果确实真的同时出现了为0的情况,这无疑说明两者x y 的赋值出现在了a b赋值之前。
package com.fds.demo.thread.test0;
/**
* @Author fandongsheng
* @Date 2024/6/23 12:35
*/
public class Main {
private static int x = 0;
private static int y = 0;
private static volatile int a = 0;
private static volatile int b = 0;
private static int c = 0;
private static int d = 0;
private static int e = 0;
private static int f = 0;
private static long count = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
x = 0;
y = 0;
a = 0;
b = 0;
c = 0;
d = 0;
e = 0;
f = 0;
count++;
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
c = 101;
d = 102;
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
e = 101;
f = 102;
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
String info = "count " + count + " x" + x + " y" + y;
if (x == 0 && y == 0) {
System.err.println(info);
break;
} else {
System.out.println(info);
}
}
}
}
给a,b使用volatile修饰一下。意思为a,b变量执行行之前的代码不可以重排到执行行之后,同时执行行之后的代码也不可以重排到执行行之前。这样a=1,b=1绝对是第一行执行的。
同样被synchronized锁住的代码块,执行行之前的代码不可以重排到执行行之后,同时执行行之后的代码也不可以重排到执行行之前。
验证:略(你可以自己动手试试,我反正是懒得写)
那么为什么会出现代码重排的现象呢?
指令重排是JVM和CPU为了提高性能而采取的一种优化手段,但在多线程环境下可能会导致数据不一致问题。为了避免这些问题,我们应该使用适当的同步机制来确保共享变量的正确访问和操作顺序。
在单例模式中可能会存在下面的情况
还有一种场景通常new一个对象我们的顺序是1:分配内存、2:对象初始化、3:简历指针对应关系。但是cpu可能优化为132的执行顺序,没有初始化就给了引用,别的线程可能存在取到没有初始化值的情况。所以要加,但是上面的情况真的只是理论上有问题。
JAVA线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?
1JAVA的锁就是在对象的Markword中记录
锁状态。无锁,偏向锁,轻量级锁,重量级锁对应不同的锁状态。
2、JAVA的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。
首先思科一个问题:JAVA中synchronized锁为什么所有的对象都可以当做锁。回答这个问题涉及到JAVA对象的一个结构,每一个对象分为三部分,对象头,对象体,补充长度。而锁就是存在于对象头中的。
在jdk1.6之后
首先看
jvm参数如果打开了偏向锁(默认不打开)当我线程运行超过4秒之后就会变为偏向锁。第一个线程执行不会有什么问题,当第二线程来的时候查看锁还在就会
3:升级过程如下:
谈谈你对AQS的理解。AQS如何实现可重入锁?
1AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。
2、在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不用的意义。
3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1。释放锁state就减1。
有A,B,C三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?如何保证三个线程有序交错进行?
如何保证线程依次执行
方法一使用join,方法join()的作用是等待线程对象销毁。内部的原理是join内部调用了wait()方法,会释放锁。还有当线程消耗时会同步主内存中的数据。
package com.fds.demo.thread.test1;
/**
* @Author fandongsheng
* @Date 2024/6/24 15:39
*/
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1--running");
}, "t1");
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2--running");
}, "t2");
Thread t3 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t3--running");
}, "t3");
t1.start();
t2.start();
t3.start();
}
}
使用semaphore
package com.fds.demo.thread.test1;
import java.util.concurrent.Semaphore;
/**
* @Author fandongsheng
* @Date 2024/6/24 15:49
*/
public class SemaphoreEX {
public static void main(String[] args) throws InterruptedException {
Semaphore s1 = new Semaphore(1);
Semaphore s2 = new Semaphore(2);
s1.acquire();
s2.acquire();
Thread t1 = new Thread(() -> {
System.out.println("t1--running");
s1.release();
}, "t1");
Thread t2 = new Thread(() -> {
try {
s1.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2--running");
s2.release();
}, "t2");
Thread t3 = new Thread(() -> {
try {
s2.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t3--running");
}, "t3");
t1.start();
t2.start();
t3.start();
}
}
使用CompletableFuture
package com.fds.demo.thread.test1;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* @Author fandongsheng
* @Date 2024/6/24 15:54
*/
public class CompletableFutureEX {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> f1 = CompletableFuture.runAsync(()->{
System.out.println("t1--running");
});
CompletableFuture<Void> f2 = f1.thenRun(() -> {
System.out.println("t2--running");
});
CompletableFuture<Void> f3 = f2.thenRun(() -> {
System.out.println("t3--running");
});
f3.get();
}
}
如何一起启动
CyclicBarrier
package com.fds.demo.thread.test1;
import java.time.LocalDateTime;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* @Author fandongsheng
* @Date 2024/6/24 16:07
*/
public class CyclicBarrierEX {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
System.out.println("原神启动");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":" + LocalDateTime.now());
}, "t" + i).start();
}
}
如何对字符串进行快速排序
思路我充分利用CPU的多核的特性,将一个大任务拆分为若干小任务分别计算,然后合并。
package com.fds.demo.thread.test1;
/**
* @Author fandongsheng
* @Date 2024/6/24 16:23
*/
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.Arrays;
public class ForkJoinMergeSort extends RecursiveAction {
private static final long serialVersionUID = 1L;
private final int[] array;
private final int lo;
private final int hi;
public ForkJoinMergeSort(int[] array) {
this.array = array;
this.lo = 0;
this.hi = array.length - 1;
}
private ForkJoinMergeSort(int[] array, int lo, int hi) {
this.array = array;
this.lo = lo;
this.hi = hi;
}
@Override
protected void compute() {
if (hi - lo < 1) {
return;
}
int mid = (lo + hi) >>> 1;
ForkJoinMergeSort leftSort = new ForkJoinMergeSort(array, lo, mid);
ForkJoinMergeSort rightSort = new ForkJoinMergeSort(array, mid + 1, hi);
invokeAll(leftSort, rightSort);
merge(lo, mid, hi);
}
private void merge(int lo, int mid, int hi) {
int[] temp = Arrays.copyOf(array, array.length);
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++) {
if (i > mid) {
temp[k] = array[j++];
} else if (j > hi) {
temp[k] = array[i++];
} else if (array[i] < array[j]) {
temp[k] = array[i++];
} else {
temp[k] = array[j++];
}
}
System.arraycopy(temp, lo, array, lo, hi - lo + 1);
}
public static void main(String[] args) {
int[] data = {4, 3, 2, 10, 12, 1, 9, 8, 7, 6};
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new ForkJoinMergeSort(data));
pool.shutdown();
System.out.println(Arrays.toString(data));
}
}