Java并发总结

Posted yuwenS.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发总结相关的知识,希望对你有一定的参考价值。

1、Java并发

1.1、使用线程

使用线程有三种方法:

继承Therad类

实现Runnable接口

实现Callable接口

继承Thread类:

需要实现Run()方法,因为Thread类实现了Runnable接口

当调⽤ start() ⽅法启动⼀个线程时,虚拟机会将该线程放⼊就绪队列中等待被调度,当⼀个线程被调度 时会执⾏该线程的 run() ⽅法

public class MyThread extends Thread {
	public void run() {
		// ...
	}
}
public static void main(String[] args) {
 	MyThread mt = new MyThread();
 	mt.start();
}

t.start()方法会创建一个新栈,然后立马结束,从而实现并发

实现接口:

实现 Runnable 和 Callable 接⼝的类只能当做⼀个可以在线程中运⾏的任务,不是真正意义上的线程, 因此最后还需要通过 Thread 来调⽤。可以理解为任务是通过线程驱动从⽽执⾏的

实现Runnable接口:

需要实现接⼝中的 run() ⽅法

public class MyRunnable implements Runnable {
	@Override
 	public void run() {
 		// ...
 	}
}

使⽤ Runnable 实例再创建⼀个 Thread 实例,然后调⽤ Thread 实例的 start() ⽅法来启动线程

public static void main(String[] args) {
	MyRunnable instance = new MyRunnable();
	Thread thread = new Thread(instance);
	thread.start();
}

实现Callable接口:

与 Runnable 相⽐,Callable 可以有返回值,返回值通过 FutureTask 进⾏封装

public class MyCallable implements Callable<Integer> {
 	public Integer call() {
        return 123;
 	}
}
public static void main(String[] args) throws ExecutionException,
InterruptedException {
 	MyCallable mc = new MyCallable();
 	FutureTask<Integer> ft = new FutureTask<>(mc);
 	Thread thread = new Thread(ft);
 	thread.start();
 	System.out.println(ft.get());
}

接口实现比较常用,因为Java是单继承的,如果使用继承那么就不能继承别的类。而接口实现可以去继承别的类

线程生命周期:线程生命周期有5大状态

新建状态:刚刚new出来的线程

就绪状态:该状态的线程已经准备就绪,可以抢夺cpu时间片

运行状态:该状态是就绪状态的线程抢到了cpu时间片,当之前的时间片用完之后又会回去就绪状态抢夺时间片

阻塞状态:阻塞状态的线程会放弃之前占有的cpu时间片(遇到了阻塞时间会进入该状态,如键盘输入、sleep()方法)当阻塞状态解除后就会回到就绪状态去抢夺时间片

死亡状态:run结束,线程消失

1.2、方法

sleep()方法出现在哪个线程中,哪个线程就休眠跟 对象调用无关,会转化为Thread.sleep(),而不是对象休眠

实例方法:

设置线程的优先级:setPriority(int newPriority)

获取线程优先级:getPriority()

最低优先级1

默认优先级5

最高优先级10

合并线程:线程.join()

当前线程进入阻塞状态,调用join()的线程执行,直到该线程执行完成,当前线程才可以继续执行

静态方法:

线程让位:yield()

让另一个线程执行,但是让位线程不是进入阻塞状态,而是重新进入就绪状态

1.3、互斥同步

1.3.1、线程同步机制(synchronized)

线程同步机制的语法:

synchronized(共享对象){
	//线程同步代码块
}

synchronized()表明线程允许时必须拿到共享变量的锁才能继续执行,在遇到synchronized时相当于进入了阻塞状态,需要拿到锁才能进入就绪状态去抢夺时间片

synchronized(共享对象) 中的共享对象,是你自己定义想要哪几个线程共享的对象,这样那几个线程就会进行线程排队

synchronized同步一个类时,两个线程调用同一个类的不同对象,也会进行线程等待排队

synchronized(类名.class){
	//线程同步代码块
}

Java中的三大变量

实例变量:在堆中

静态变量:在方法区中

局部变量:在栈中

在三大变量中:局部变量永远不会存在线程安全问题,因为局部变量不共享,(一个线程一个栈)

如果锁在了静态变量中相当于拿到到的是类锁

在开发中如何解决线程安全问题

尽量使用局部变量代替实例变量

如果必须是实例变量,就尽量创建多个对象

如果必须是实例变量并且只能创建一个对象,那么就用synchronized()锁住同步代码块

1.3.2、ReentrantLock

跟synchronized类似,都是通过锁住线程同步语句块来保证线程安全,但是synchronized是自动释放锁,而ReentrantLock需要手动释放锁

public class ReentrantLockTest {
    private Lock lock = new ReentrantLock();
    public void test(){
        lock.lock();
        try {
            System.out.println("来锁住我吧");
        }finally {
            lock.unlock();  //手动释放锁,避免死锁的产生
        }
    }
}

1.3.3、二者比较

锁的实现:

synchronized 是 JVM 实现的,⽽ ReentrantLock 是 JDK 实现的

性能:

新版本 Java 对 synchronized 进⾏了很多优化,例如⾃旋锁等,synchronized 与 ReentrantLock⼤致相同

等待可中断:

当持有锁的对象长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其它的事情,ReentrantLock可中断,而synchronized 不行

公平锁:

公平锁是指在多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁

synchronized 中的锁的不公平的,ReentrantLock默认情况下也是非公平的,但是也可以是公平的

使用选择:

除⾮需要使⽤ ReentrantLock 的⾼级功能,否则优先使⽤ synchronized。这是因为 synchronized 是 JVM 实现的⼀种锁机制,JVM 原⽣地⽀持它,⽽ ReentrantLock 不是所有的 JDK 版本都⽀持。并且使 ⽤ synchronized 不⽤担⼼没有释放锁⽽导致死锁问题,因为 JVM 会确保锁的释放

1.4、守护线程

具有代表性的守护线程:垃圾回收线程

将线程设置为守护线程:线程.setDaemon(true);

所有线程结束时,守护线程也会结束,无论守护线程是否还在运行

1.5、定时器

间隔特定的时间,执行特定的程序

1.6、关于Object中的wait()和notify()方法

wait()和notify()方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,应为这两个方法是Object自带的

wait()方法:让正调用的对象上活动的线程进入等待状态,无限期等待,直到被唤醒为止。并且还会释放锁

notify()方法:唤醒调用对象上活动的线程

notifyAll()方法:唤醒所有进入等待状态的线程

与wait()和notify()类似的方法

await() signal() signalAll()个方法

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调⽤ await() ⽅法使线程等待,其它线程调⽤ signal() 或 signalAll() ⽅法唤醒等待的线程。 相⽐于 wait() 这种等待⽅式,await() 可以指定等待的条件,因此更加灵活。 使⽤ Lock 来获取⼀个 Condition 对象

1.7、线程池

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务

ExecutorService:用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理

  • execute(Runnable任务对象) 把任务丢到线程池

Executors:辅助创建线程池的工具类

  • newFixedThreadPool(int nThreads) 最多n个线程的线程池
  • newCachedThreadPool() 足够多的线程,使任务不必等待
  • newSingleThreadExecutor() 只有一个线程的线程池
 public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"------->"+"hello");
                }
            });
        }
        pool.shutdown();
    }

1.8、Java内存模型

Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达 到⼀致的内存访问效果

1.8.1、内存间交互操作(8大操作)

read:把一个变量从主内存中传递到工作内存中

load:在read之后执行,把read得到的值放入工作内存变量副本之中

use:把工作内存中一个变量的值传递给执行引擎

assign:把⼀个从执⾏引擎接收到的值赋给⼯作内存的变量

store:把⼯作内存的⼀个变量的值传送到主内存中

write:在 store 之后执⾏,把 store 得到的值放⼊主内存的变量中

lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态

unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来 , 释放后的变量才可以被其他线程锁定unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来 , 释放后的变量才可以被其他线程锁定

1.8.2、内存模型的三大特性

原子性: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。最常遇到的 就是回滚操作解决这个问题

Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

AtomicXXX 类也能保证原子性

可见性: 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

Java提供了volatile关键字来保证可见性,当一个共享变量被volatile修饰时,它保证本修改的值会被立刻更新到主存。当有其它线程去读取时,会去主存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性

通过synchronized和Lock也能保证可见性,因为synchronized和Lock能保证同一时刻只有一个线程获取锁任何执行同步代码,并且会在释放锁之前将变量的修改更新到主存中,所有能保证可见性

有序性: 指在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,即程序执行的顺序按照代码的先后顺序执行,一般情况下,处理器由于要提高执行效率,对代码进行重排序,运行的顺序可能和代码先后顺序不同,但是结果一样。单线程下不会出现问题,多线程就会出现问题了

volatile 关键字通过添加内存屏障的⽅式来禁⽌指令重排,即重排序时不能把后⾯的指令放到内存屏障 之前。 也可以通过

synchronized 来保证有序性,它保证每个时刻只有⼀个线程执⾏同步代码,相当于是让线 程顺序执⾏同步代码

1.9、锁优化

这里所说的是JVM对synchronized锁优化

1.9.1、自旋锁

互斥同步进⼊阻塞状态的开销都很⼤,应该尽量避免。在许多应⽤中,共享数据的锁定状态只会持续很 短的⼀段时间,为了避免在这个短时间内进入阻塞,就引入了自旋锁。自旋锁的思想就是让一个线程请求一个共享数据时等待一段时间(自旋)(无意义的循环),看持有锁的线程是否会很快释放锁

自旋锁虽然能避免进入阻塞状态而减少开销,但是它需要进行忙循环操作占用cpu时间,它只适用共享数据锁定状态很短的场景

1.9.2、锁消除

锁消除是检测出来不存在竞争的共享数据的锁进行消除。锁消除的依据是逃逸分析的数据支持

1.9.3、锁粗化

如果一系列的连续操作都对同一个对象进行反复的加锁和解锁,频繁的加锁就会导致性能损耗

1.9.4、轻量级锁

引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。其对锁的获取和释放依靠更加轻量的 cas 实现,从而减少性能损耗

1.9.5、偏向锁

偏向锁的思想是偏向于让第⼀个获取锁对象的线程,这个线程在之后获取该锁就不再需要进⾏同步操 作,甚⾄连 CAS 操作也不再需要

以上是关于Java并发总结的主要内容,如果未能解决你的问题,请参考以下文章

经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!

线程学习知识点总结

全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段

并发编程:我对Java并发编程的总结和思考

关于Java并发编程的总结和思考

golang goroutine例子[golang并发代码片段]