一、创建多线程的三种方式
1.继承Thread类,重写run方法
class Demo extends Thread{
@Override
public void run() {
System.out.println("这是一个子线程-Thread");
}
}
//启动方式
Demo demo = new Demo();
new Thread(demo).start();
2.实现Runnable接口,实现run方法
因为java是单继承,多实现而繁生的Runnable接口
class Demo implements Runnable {
@Override
public void run() {
System.out.println("这是一个子线程--Runnable");
}
}
//启动方法
Demo demo = new Demo();
new Thread(demo).start();
3.实现Callable接口,实现call方法(有返回值)
class CallableTest implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("这是一个子线程");
return "返回值";
}
}
//启动方法
CallableTest callableTest = new CallableTest();
FutureTask<String> futureTask = new FutureTask<>(callableTest);
new Thread(futureTask).interrupt();
//获取线程返回值 会一直阻塞当前线程直到拿到返回值
String str = futureTask.get();
二、多线程停止方法(stop、interrupt )
1.stop方法(已过时)
为什么弃用stop:
- 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件流,数据库等的关闭。
- 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
官方说法:Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?
2.interrupt 线程终止方法
interrupt是协作式方法 具体是否停止由线程自体方法决定。
设计思想:为了让线程有足够的时间去做清理工作 如文件流、数据库关闭。
源码:
源码中指出interrupt()方法只会设置一个终止的标志,并不会对线程进行操作
下面用例子测试说明
class InterruptThread1 extends Thread{
public static void main(String[] args) {
try {
InterruptThread1 t = new InterruptThread1();
t.start();
Thread.sleep(200);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i=" + i);
}
}
}
可以看到线程并未因为执行interrupt方法而停止。(此处我是手动终止程序)
那么怎么使线程停止?
isInterrupted()方法:获取当前线程中断标志;
static interrupted()方法:获取当前线程中断标志,并把中断标志改为false;
下面看一下使用:
class InterruptThread1 extends Thread{
public static void main(String[] args) {
try {
InterruptThread1 t = new InterruptThread1();
t.start();
Thread.sleep(200);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
if(isInterrupted()){
break;
}
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i=" + i);
}
}
}
当执行interrupt方法线程会停止。
注意:以上代码中如果sleep(1)抛出InterruptedException异常时 中断标志会被置为false,所以需要在catch里再次执行interrupt
三、sleep、wait、yield区别
1.sleep
sleep方法相当于线程的睡眠,睡眠期间会释放资源让CPU处理其他事情,但不会释放锁且当前线程会一直处于阻塞状态。
2.wait
wait方法会释放锁释放CPU资源,线程处于阻塞状态,等待唤醒。
notify():唤醒一个处于等待状态的线程,在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
3.yield
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。而且,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
四、多线程线程安全
1.synchronized锁
在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
public synchronized void test(){
}
静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
public static synchronized void test(){
}
同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
private Object object = new Object();
public void test(){
synchronized (object){
}
}
2.volatile关键字
作用:保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。
注意:volatile只能保证变量的可见性,不能保证对volatile变量操作的原子性;
volatile适用场景是单线程写,多线程读
3.ThreadLocal变量
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
//创建一个ThreadLocal
static ThreadLocal<String> localVar = new ThreadLocal<>();
set方法源码:
public void set(T value) {
//(1)获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//(4)如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}
以当前线程为key,变量为value的map集合
以此分隔不同线程对应的变量来达到线程安全的目的。
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是
- Synchronized是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
细文推荐:https://www.jianshu.com/p/3c5d7f09dfbd
4.join
public class Test {
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
Thread t1 = new Thread(threadTest);
t1.join();
}
}
class ThreadTest extends Thread{
@Override
public void run() {
System.out.println("线程");
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当主线程执行到join方法时,会阻塞等待t1线程执行完后,才会继续执行下去。
五、fork/join 分而治之(大任务拆分多个子任务)
理念:把大的任务拆分成多个小任务,然后把多个小任务的结果合成一个大的结果
1.同步执行 带有返回值
继承RecursiveTask 泛型为返回结果类型
public class Test {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
int arr[] = {1,2,3,4,5,6,7,8,9,0};
Demo demo = new Demo(arr,0,arr.length-1);
forkJoinPool.invoke(demo);
}
}
class Demo extends RecursiveTask<Integer> {
//设置一个阈值
private final static int THRESHOLD = 400;
//需要求和的数组
private int[] arr;
//开始下标 具体由arr总长度和阈值拆分
private int startIndex;
//结束下标
private int endIndex;
public Demo(int[] arr, int startIndex, int endIndex) {
this.arr = arr;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
protected Integer compute() {
if (endIndex - startIndex < THRESHOLD) {
int sum=0;
for(int i = startIndex;i<=endIndex;i++){
sum+=arr[i];
}
return sum;
}else{
//自定递归拆分规则
int middle = (startIndex+endIndex)/2;
Demo demo = new Demo(arr,startIndex,middle);
Demo demo2 = new Demo(arr, middle+1,endIndex);
//批量提交
invokeAll(demo,demo2);
return demo.join()+demo2.join();
}
}
}
2.异步执行 无返回值
继承RecursiveAction
public class Test {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
int arr[] = {1,2,3,4,5,6,7,8,9,0};
Demo demo = new Demo(arr,0,arr.length-1);
forkJoinPool.invoke(demo);
}
}
class Demo extends RecursiveAction {
//设置一个阈值
private final static int THRESHOLD = 400;
//需要求和的数组
private int[] arr;
//开始下标 具体由arr总长度和阈值拆分
private int startIndex;
//结束下标
private int endIndex;
public Demo(int[] arr, int startIndex, int endIndex) {
this.arr = arr;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
protected void compute() {
if (endIndex - startIndex < THRESHOLD) {
for(int i = startIndex;i<=endIndex;i++){
System.out.println(arr[i]);
}
}else{
//自定递归拆分规则
int middle = (startIndex+endIndex)/2;
Demo demo = new Demo(arr,startIndex,middle);
Demo demo2 = new Demo(arr, middle+1,endIndex);
//批量提交
invokeAll(demo,demo2);
}
}
}
注意:多线程上下文切换会消耗一定性能,如果“大任务”本身耗时不长,首选单线程。