多线程
多线程
进程和线程
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
进程与线程最主要的区别是它们是操作系统管理资源的不同方式的体现。 准确来说进程与线程属于衍生关系。 进程是操作系统执行程序的一次过程,在这个过程中可能会产生多个线程。
比如在使用QQ时,有窗口线程, 文字发送的线程,语音输入的线程。(可能不是很恰当)
大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,多个进程。
Lword如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,两个线程:容灾备份,语法检查
由于系统在线程之间的切换比在进程之间的切换更高效率,所以线程也可以被视为轻量级进程。
- 进程就是后台运行的程序。
- 线程就是轻量级的进程,依附于某一个进程,共享该进程的内存资源。
并发和并行
并发:多个线程任务被一个cpu轮流执行,即多个线程同一时间点争取同一个资源。注意,这里并不是只允许一个cpu执行多任务, 多个cpu执行也是可以的。并发强调的是计算机应用程序有处理多个任务的能力。
并行:多个线程任务被多个cpu同时执行。这里也并不是只允许多个cpu处理多任务,一个cpu也是可以的, 只要cpu能在同一时刻处理多任务。并行强调的是计算机应用程序拥有同时处理多任务的能力。
总结: 并行包含并发。
多线程的利弊
利:线程可以比作轻量级的进程,cpu在线程之间的切换比在进程之间的切换,耗费的资源要少的多。现在是多核cpu时代,意味着多个线程可以被多个cpu同时运行(并行),如果可以利用好多线程,那么可以编写出高并发的程序。
弊:虽然线程带来的好处很多,但是并发编程并不容易,如果控制不好线程,那么就可能造成死锁,资源闲置,内存泄露等问题。
什么是上下文切换?
cpu是采用时间片的轮转制度,在多个线程之间来回切换运行的。 当cpu切换到另一个线程的时候,它会先保存当前线程执行的状态, 以便在下次切换回来执行时,可以重新加载状态,继续运行。 从保存线程的状态再到重新加载回线程的状态的这个过程就叫做上下文切换。
线程的优先级
在Java中可以通过Thread类的setPriority方法来设置线程的优先级, 虽然可以通过这样的方式来设置线程的优先级,但是线程执行的先后顺序并不依赖与线程的优先级。 换句话说就是,线程的优先级不保证线程执行的顺序。
线程的6种状态
详见:jdk Thread类源码中的state枚举类
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
Thread.State
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,(新建)
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,(准备就绪)
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,(阻塞)
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,(不见不散)
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,(过时不候)
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;(终结)
}
sleep方法和wait方法的区别
相同点:都是当前线程暂停
不同点:
-
sleep方法是Thread类的方法;而wait方法是Object类的方法。
-
sleep方法会使当前线程让出cpu的调度资源,从而让其他线程有获得被执行的机会, 但是并不会让当前线程释放锁对象(抱着锁睡觉); 而wait方法是让当前线程释放锁并进入wait状态, 不参与获取锁的争夺,从而让其他等待资源的线程有机会获取锁, 只有当其他线程调用notify或notifyAll方法是,被wait的线程才能重新与其他线程一起争夺资源。
sleep握紧锁去睡,醒了手里还有锁
wait放开锁去睡,放开手里的锁
stop,suspend,resume等方法为什么会被遗弃
stop: stop方法被弃用很好理解,因为stop方法是强行终止线程的执行, 不管线程的run方法是否执行完,资源是否释放完,它都会终止线程的运行,并释放锁。 显然,这在设计上就不合理。
suspend和resume: suspend方法用于阻塞一个线程,但并不释放锁, 而resume方法的作用只是为了恢复被suspend的线程。 假设A,B线程都争抢同一把锁,A线程成功的获得了锁, 然后被suspend阻塞了,却并没有释放锁,它需要其他线程来唤醒, 但此时B线程需要获得这把锁才能唤醒A,所以此时就陷入了死锁。
interrupt,interrupted,isInterrupted方法区别
interrupt: 这个方法并不是中断当前线程,而是给当前线程设置一个中断状态。
isInterrupted: 当线程调用interrupt方法后,线程就有了一个中断状态, 而使用isInterrupted方法就可以检测到线程的中断状态。
interrupted: 这个方法用于清除interrupt方法设置的中断状态。 如果一个线程之前调用了interrupt方法设置了中断状态, 那么interrupted方法就可以清除这个中断状态。
join方法
join方法的作用是让指定线程加入到当前线程中执行。
假如在main方法里面创建一个线程A执行,并调用A的join方法, 那么当前线程就是main,指定的A线程就会在main之前执行, 等A执行完后,才会继续执行main。
public static void main(String[] args) throws Exception
{
Thread a = new Thread(()->
{
try
{
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){}
System.out.println("thread join");
});
a.start();
//a会在main线程之前执行
a.join();
System.out.println("main");
}
join方法的底层是wait方法,调用A线程(子线程)的join方法实际上是让main线程wait, 等A线程执行完后,才能继续执行后面的代码。
yield方法
yield属于Thread的静态方法, 它的作用是让当前线程让出cpu调度资源。
yield方法其实就和线程的优先级一样,你虽然指定了, 但是最后的结果不由得你说了算, 即使调用了yield方法,最后仍然可能是这个线程先执行, 只不过说别的线程可能先执行的机会稍大一些。
Lock接口
多线程编程模板:
- 线程 操作 资源类
- 高内聚低耦合
多线程编程步骤:
- 创建资源类
- 资源类里创建同步方法、同步代码块
Lock锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。
如何使用
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
synchronized与Lock的区别
两者区别:
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
卖票问题
- 老版Synchronized写法
class Ticket{//资源类
//票
private int number = 30;
public synchronized void saleTicket(){
if (number > 0){
System.out.println(Thread.currentThread().getName()+"\t卖出第:"+(number--)+"\t还剩下:"+number);
}
}
}
/**
*题目:三个售票员 卖出 30张票
* 多线程编程的企业级套路+模板
* 1.在高内聚低耦合的前提下,线程 操作(对外暴露的调用方法) 资源类
*/
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//Thread(Runnable target, String name)
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
},"C").start();
}
}
模板抽取:
class Ticket{//资源类
//同步方法
public synchronized void saleTicket(){
}
}
//模板:线程 操作(对外暴露的调用方法) 资源类
public class SaleTicket {
Ticket ticket = new Ticket();//资源类
//Thread(Runnable target, String name)
new Thread(new Runnable() {//线程
@Override
public void run() {
//操作(对外暴露的调用方法)
}
}
},"A").start();
}
新版Lock写法,效果一样
//Class ReentrantLock是lock接口最常用的实现类之一,即可重入锁
//资源类 = 实例变量 + 实例方法
class Ticket{
//票
private int number = 30;
private Lock lock = new ReentrantLock();
public void sale(){
lock.lock();
try {
if (number > 0){
System.out.println(Thread.currentThread().getName()+"\t卖出第:"+(number--)+"\t还剩下:"+number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
/**
*题目:三个售票员 卖出 30张票
* 笔记:如何编写企业级的多线程
* 固定的编程套路+模板
* 1.在高内聚低耦合的前提下,线程 操作(对外暴露的调用方法) 资源类
* 1.1先创建一个资源类
*/
public class SaleTicketDemo1 {
//主线程,一切程序的入口
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{for (int i = 1; i <= 40; i++) ticket.sale();},"A").start();
new Thread(()->{for (int i = 1; i <= 40; i++) ticket.sale();},"B").start();
new Thread(()->{for (int i = 1; i <= 40; i++) ticket.sale();},"C").start();
}
}
模板抽取:
class Ticket{//资源类
//Class ReentrantLock是lock接口最常用的实现类之一,即可重入锁
private Lock lock = new ReentrantLock();
public void sale(){
lock.lock();//加锁
try {
//同步代码
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();//解锁
}
}
}
public class SaleTicketDemo1 {//线程 操作(对外暴露的调用方法) 资源类
//主线程,一切程序的入口
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{ticket.sale();},"A").start();
}
}
注意:.start()并非线程立即启动,而是指线程进入就绪状态,随时可进入启动状态。
上述匿名内部类的方式太冗余,我们使用Lambda表达式来简化匿名内部类中方法的书写。在后续,我们会大量使用Lambda表达式来写线程的Runnable接口的run方法(推荐使用,效果一样:)
//资源类 = 实例变量 + 实例方法
class Ticket{
//票
private int number = 30;
Lock lock = new ReentrantLock();
public void sale(){
lock.lock();
try {
if (number > 0){
System.out.println(Thread.currentThread().getName()+"\t卖出第:"+(number--)+"\t还剩下:"+number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
/**
*题目:三个售票员 卖出 30张票
* 笔记:如何编写企业级的多线程
* 固定的编程套路+模板
* 1.在高内聚低耦合的前提下,线程 操作(对外暴露的调用方法) 资源类
* 1.1先创建一个资源类
*/
public class SaleTicketDemo1 {
//主线程,一切程序的入口
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{for (int i = 1; i <= 40; i++) ticket.sale();},"A").start();
new Thread(()->{for (int i = 1; i <= 40; i++) ticket.sale();},"B").start();
new Thread(()->{for (int i = 1; i <= 40; i++) ticket.sale();},"C").start();
}
}
Lambda表达式:
Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或剪头操作符。它将 Lambda 分为两个部分:
左侧:指定了 Lambda 表达式需要的所有参数
右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能
lambda表达式,如果一个接口只有一个方法,我可以把方法名省略
Foo foo = () -> {System.out.println(“****hello lambda”);};
lambda表达式,必须是函数式接口,必须只有一个方法。如果接口只有一个方法java默认它为函数式接口。为了正确使用Lambda表达式,需要给接口加个注解:@FunctionalInterface。如有两个方法,立刻报错
Runnable接口为什么可以用lambda表达式?
(1)口诀:
拷贝小括号,写死右键头,落地大括号
(2) @FunctionalInterface
若接口只有一个方法(且是未实现的方法),那么可以不用显式在接口上加该函数式接口的注解,默认实现了。但是如果存在多个方法(无论是否实现),那么就必须显式声明函数式接口,不过,Lambda表达式忽略default的实现方法和静态方法,他只关注仅有的一个未实现方法
(3)default
Java8之前接口中只能有方法的声明,不能有实现。Java8之后,可以使用default支持部分方法的实现,且可以有多个default默认实现
(4)static
静态方法,静态方法同样可以实现多个
interface Foo{
public void sayhello();
}
public class LambdaExpressDemo{
public static void main(String[] args){
//拷贝小括号,写死右键头,落地大括号
Foo foo = () -> {
System.out.println("hello");
}
foo.sayhello();
}
}
interface Foo{
public int add(int x, int y);
}
public class LambdaExpressDemo{
public static void main(String[] args){
//拷贝小括号,写死右键头,落地大括号
Foo foo = (int x,int y) -> {
System.out.println("hello");
return x + y;
}
System.out.println(foo.add(3, 5));
}
}
//显式声明函数式接口
@FunctionalInterface
interface Foo{
public int add(int x, int y);
//声明和实现
default int div(int x, int y){
System.out.println("hello");
return x / y;
}
}
public class LambdaExpressDemo{
public static void main(String[] args){
Foo foo = (x, y) -> {
System.out.println("hello");
return x + y;
}
System.out.println(foo.add(3, 5));
System.out.println(foo.div(3, 5));
}
}
//显式声明函数式接口
@FunctionalInterface
interface Foo{
public int add(int x, int y);
//声明和实现
default int div(int x, int y){
System.out.println("hello");
return x / y;
}
public static int mv(int x, int y){//静态方法同样可以多个
System.out.println("hello");
return x * y;
}
}
public class LambdaExpressDemo{
public static void main(String[] args){
Foo foo = (x, y) -> {
System.out.println("hello");
return x + y;
}
System.out.println(foo.add(3, 5));
System.out.println(foo.div(3, 5));
Foo.mv(3, 5);//静态方法直接用类调用
}
}
线程间通信
多线程编程模板:判断-干活-通知
生产者消费者
老版synchronized写法
class Aircondition{
private int number = 0;
//老版写法
public synchronized void increment() throws Exception{
//1.判断
while (number != 0){//必须用while,解决虚假唤醒
this.wait();
}
//2.干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
this.notifyAll();
}
public synchronized void decrement() throws Exception{
//1.判断
while (number == 0){
this.wait();
}
//2.干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
this.notifyAll();
}
}
/**
* 题目:现在两个线程,可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量-1,
* 实现交替,来10轮,变量初始值为0.
* 1.高内聚低耦合前提下,线程操作资源类
* 2.判断/干活/通知
* 3.多线程交互中,必须要防止多线程的虚假唤醒,也即:判断只能用while,不能用if
* 知识小总结:多线程编程套路+while判断+新版写法
*/
public class ProdConsumerDemo4 {
public static void main(String[] args) {
Aircondition aircondition = new Aircondition();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"D").start();
}
}
新版写法
class Aircondition{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//新版写法
public void increment() throws Exception{
lock.lock();
try {
//1.判断
while (number != 0){
condition.await();//this.wait()被替换
}
//2.干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
condition.signalAll();//this.notifyAll()被替换
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws Exception{
lock.lock();
try {
//1.判断
while (number == 0){
condition.await();
}
//2.干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 题目:现在两个线程,可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量-1,
* 实现交替,来10轮,变量初始值为0.
* 1.高内聚低耦合前提下,线程操作资源类
* 2.判断/干活/通知
* 3.防止虚假唤醒(判断只能用while,不能用if)
* 知识小总结:多线程编程套路+while判断+新版写法
*/
public class ProdConsumerDemo4 {
public static void main(String [] args) {
Aircondition aircondition = new Aircondition();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"D").start();
}
}
线程间定制化调用通信
1、有顺序通知,需要有标识位
2、有一个锁Lock,3把钥匙Condition
3、判断标志位
4、输出线程名+第几次+第几轮
5、修改标志位,通知下一个
精确通知顺序访问
使用Lock锁以及await、signal实现 精确通知顺序访问。这也是相较于synchronized以及wait、notifyAll的优势所在。
class ShareData{
private int number = 1;//A:1,B:2,C:3
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void printc1(){
lock.lock();
try {
//1.判断
while (number != 1){
c1.await();
}
//2.干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 2;
//通知第2个
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printc2(){
lock.lock();
try {
//1.判断
while (number != 2){
c2.await();
}
//2.干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 3;
//如何通知第3个
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printc3(){
lock.lock();
try {
//1.判断
while (number != 3){
c3.await();
}
//2.干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 1;
//如何通知第1个
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 备注:多线程之间按顺序调用,实现A->B->C
* 三个线程启动,要求如下:
* AA打印5次,BB打印10次,CC打印15次
* 接着
* AA打印5次,BB打印10次,CC打印15次
* 来10轮
* 1.高内聚低耦合前提下,线程操作资源类
* 2.判断/干活/通知
* 3.多线程交互中,防止虚假唤醒(判断只能用while,不能用if)
* 4.标志位
*/
public class ConditionDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
shareData.printc1();
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
shareData.printc2();
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
shareData.printc3();
}
},"C").start();
}
}
多线程锁
八锁理论
class Phone{
public static synchronized void sendEmail() throws Exception{
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("*******sendEmail");
}
public synchronized void sendMs() throws Exception{
TimeUnit.SECONDS.sleep(2);
System.out.println("*******sendMs");
}
public void sayHello() throws Exception{
TimeUnit.SECONDS.sleep(3);
System.out.println("*****sayHello");
}
}
/**
* 1.标准访问,先打印邮件
* 2.邮件设置暂停4秒方法,先打印邮件
* 对象锁
* 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* 其他的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法,
* 锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法
* 3.新增sayHello方法,先打印sayHello
* 加个普通方法后发现和同步锁无关
* 4.两部手机,先打印短信
* 换成两个对象后,不是同一把锁了,情况立刻变化
* 5.两个静态同步方法,同一部手机,先打印邮件
* 都换成静态同步方法后,情况又发生变化
* new 锁的是this,具体的一部手机
* 静态static 锁的是class,唯一的一个模板
* 6.两个静态同步方法,同两部手机,先打印邮件,锁的同一个字节码对象
* 全局锁
* 7.一个静态同步方法,一个普通同步方法,同一部手机,先打印短信
* 前者锁的是class手机模板,后者锁的是具体的手机对象,就不是一个锁,所以不互相影响
* 8.一个静态同步方法,一个普通同步方法,同二部手机,先打印短信
* 所有的非静态同步方法用的都是同一把锁——实例对象本身。
* synchronized实现同步的基础:java中的每一个对象都可以作为锁。
* 具体表现为以下3种形式。
* 对于普通同步方法,锁是当前实例对象,锁的是当前对象this,
* 对于同步方法块,锁的是synchronized括号里配置的对象。
* 对于静态同步方法,锁是当前类的class对象
* 当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
* 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁,
* 可是别的实例对象的普通同步方法因为跟该实例对象的普通同步方法用的是不同的锁,
* 所以无需等待该实例对象已获取锁的普通同步方法释放锁就可以获取他们自己的锁。
*
* 所有的静态同步方法用的也是同一把锁--类对象本身,
* 这两把锁(this/class)是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
* 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
* 而不管是同一个实例对象的静态同步方法之间,
* 还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象
*/
public class LockBDemo05 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(()->{
try {
phone.sendMs();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
Thread.sleep(100);
new Thread(()->{
try {
phone.sayHello();
} catch (Exception e) {
e.printStackTrace();
}
},"c").start();
}
}
NotSafeDemo
集合不安全类
集合不安全类:如ArrayList、HashSet、HashMap
- 故障现象:java.util.ConcurrentModificationException(并发修改异常)
- 导致原因:其add()方法均没有加锁,因此ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException(并发修改异常)
List<String> list = new ArrayList<>();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
看ArrayList的源码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
没有synchronized线程不安全
- 解决方案:
- Vector
- Collection.synchronizedList(new ArrayList<>());
- CopyOnWriteArrayList():写时复制
- 优化建议(同样的错误,不出现第2次)
解决方法一:List list = new Vector<>();
- 因为Vector的add()方法是加锁的。
看Vector的源码
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
有synchronized线程安全
解决方法二:List list = Collections.synchronizedList(new ArrayList<>());
Collections提供了方法synchronizedList保证list是同步线程安全的,相当于给ArrayList加锁
那HashMap,HashSet是线程安全的吗?也不是
所以有同样的线程安全方法
解决方法三:List list = new CopyOnWriteArrayList<>();//写时复制
不加锁性能提升出错误,加锁数据一致性能下降。
CopyOnWriteArrayList是arraylist的一种线程安全变体,其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。
CopyOnWriteArrayList类add()方法源码:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
private static final Object PRESENT = new Object();
//调用HashMap
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
//value为Object类型的常量
return map.put(e, PRESENT)==null;
}
}
总结:对集合不安全类ArrayList、HashSet、HashMap的安全处理
/**
* 1.故障现象
* 并发修改异常
* java.util.ConcurrentModificationException
* 2.导致原因
* 3.解决方法
* 3.1 new Vector<>();
* 3.2 Collections.synchronizedList(new ArrayList<>());
* 3.3 new CopyOnWriteArrayList(); //写时复制
* 4.优化建议(同样的错误不犯第二次)
*
* 写时复制:
* CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是现将当前容器Object[]进行Copy,
* 复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加完元素之后,
* 再将原容器的引用指向新的容器setArray(newElements);。这样做的好处是可以对CopyOnWrite容器进行并发的读,
* 而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器
*/
public class NotSafeDemo3 {
public static void main(String[] args) {
mapNotSafe();
}
public static void mapNotSafe() {
Map<String,String> map = new ConcurrentHashMap<String, String>();
//Collections.synchronizedMap(new HashMap<>())
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,8));
System.out.println(map);
},String.valueOf(i)).start();
}
}
public static void setNotSafe() {
Set<String> set = new CopyOnWriteArraySet<String>();
//new Vector<>();
//Collections.synchronizedSet(new HashSet<>());
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(set);
},String.valueOf(i)).start();
}
}
public static void listNotSafe() {
List<String> list = new CopyOnWriteArrayList();
//new Vector<>();
//Collections.synchronizedList(new ArrayList<>());
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
扩展类比
HashSet和HashMap
Set<String> set = new HashSet<>();//线程不安全
Set<String> set = new CopyOnWriteArraySet<>();//线程安全
Map<String,String> map = new HashMap<>();//线程不安全
Map<String,String> map = new ConcurrentHashMap<>();//线程安全
HashSet的底层实现即为HashMap,HashSet底层的add()方法就是HashMap的put()方法,只不过add的元素就是HashMap的put()方法的key,其value固定是一个Object类型的常量。HashSet的add是放一个值,而HashMap是放K、V键值对。
HashMap的底层是Node类型的数组+Node类型的链表+红黑树。(数组+链表+红黑树)
HashMap默认初始容量为16,默认的负载因子为0.75,即默认容量达到16*0.75=12时开始扩容
ArrayList扩容为原来的一半,HashMap扩容为原来的一倍
可见HashMap里存放的是一个个Node节点,即将我们熟知的key、value存放在了Node数组中。当hash相同时,比较key,key相同(equals()方法)则覆盖前一个,key不相同变链表,即形成Node节点的单向链表,JDK1.8及其以后形成链表采用“尾插法”,即新增在链表上元素的位置为链表尾部。
链表遍历查找元素是比较慢的,在HashMap中put元素发现数组桶位上已有元素,接着遍历桶位上的链表查找是否有相同key的过程称为hash碰撞,这是比较耗性能的。而为了避免链表过长遍历时间过大的问题,在JDK1.8采用了数组+链表+红黑树的结构,在往链表上新增元素时发现链表长度超过8时,会进入链表转红黑树的方法,然后再判断数组长度是否不小于64,若满足条件则将链表转化为红黑树。而当红黑树的节点<6时,会由红黑树转换为链表,这就是二者的性能临界点。使用红黑树的目的即为了解决链表过长性能低的问题。
public HashSet() {
map = new HashMap<>();
}
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
//可见,HashSet底层是HashMap,不过HashSet对应的value是一个固定值
}
多线程创建
一、获得多线程的方法有几种?
共有四种。传统的是继承thread类和实现runnable接口,java5以后又有实现callable接口和java的线程池获得。
二、继承Thread类
1、步骤
1. 自定义一个类继承Thread类。
2. 重写Thread类的run方法 , 把自定义线程的任务代码写在run方法中
3. 创建Thread的子类对象,并且调用start方法开启线程。
疑问: 重写run方法的目的是什么?
每个线程都有自己的任务代码,jvm创建的主线程的任务代码就是main方法中的所有代码, 自定义线程的任务代码就写在run方法中,自定义线程负责了run方法中代码。
2、注意:
一个线程一旦开启,那么线程就会执行run方法中的代码,run方法千万不能直接调用,直接调用run方法就相当调用了一个普通的方法而已并没有开启新的线程。
原因:
查看start方法源码,
Start(){
….
start0(); //是一个本地方法,其调用了run方法。
}
这是一种模板设计模式,类似netty中的继承一些入栈handler,重写读和写方法一样。
public void run() {
if (target != null) {
target.run();
}
}
注意:这里的target是Thread维护的一个Runnable接口的引用,Runnable target.
如果从构造函数中传来了Runnable接口的实现类,那么target会引用到这个实现类,在run方法中调用了实现类的run。
如果没有传入runnable,那么就要重写这个run方法,由JVM调用。
三、实现runnable接口
注意:runnable不是线程,线程只有Thread。runnable只是一个接口 ,可以把线程的任务单独提出来。runnable将可执行的逻辑单元和线程控制分离开来,这也是面向对象思想的体现。(将Runnable实现类对象定义为final,即多个线程操控的是同一个Runnable实现类对象,同一份业务单元,当然,这里也会存在线程安全问题,后续再讲)
1、步骤
1. 自定义一个类实现Runnable接口。
2. 实现Runnable接口(只有一个run方法)的run方法,把自定义线程的任务定义在run方法上。
3. 创建Runnable实现类对象。
4. 创建Thread类的对象,并且把Runnable实现类的对象作为实参传递。
5. 调用Thread对象 的start方法开启一个线程。
public class TestDemo implements Runnable {
public void run() {
}
public static void main(String[] args) {
new Thread(new TestDemo()).start();
}
}
2、实现方式
- 新建类实现runnable接口
class MyThread implements Runnable//新建类实现runnable接口
new Thread(new MyThread,...)
这种方法会新增类,有更新更好的方法
- 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
}
}, "your thread name").start();
这种方法不需要创建新的类,可以new接口
- lambda表达式
new Thread(() -> {
}, "your thread name").start();
这种方法代码更简洁精炼
四、实现Callable接口,本质还是runnable
这是一个函数式接口,因此可以用作lambda表达式或方法引用的赋值对象。
1、步骤
1.自定义一个类实现callable接口
2.实现call方法
3.创建futuretask对象,把自定义类当做构造参数传进去
4.创建一个线程 new thread,把futuretask当做构造参数传进去。
5.调用futuretask的run方法。
public void run() {
…..
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
执行完毕后,把返回值set到result中。
set(result);
}
} finally {
…..
}
}
//创建新类MyThread实现runnable接口
class MyThread implements Runnable{
@Override
public void run() {
}
}
//新类MyThread2实现callable接口
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("******come in call method()");
return 1080;
}
}
/**
* 多线程中,第3种获得多线程的方式
*/
public class CallableDemo7 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask(new MyThread2());
new Thread(futureTask,"A").start();
//get方法放到最后,否则会阻塞主线程的执行,影响效率
Integer result = futureTask.get();//获得call()方法的返回值
System.out.println(result);
}
}
Thread的构建只有利用Runnable接口,因此需要Runnable接口和Callable接口的“中间人”,即FutureTask类,实现了Runnable的子接口,同时可以接受Callable接口实现类的形参。因此,即可使用Callable接口生成线程。本质上还是利用了Java的多态性质。
FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread());
new Thread(ft, "AA").start();
运行成功后如何获得返回值?
ft.get();
FutureTask实现了callable和ruannable接口,其run方法本质还是runnable的run方法。thread调用的是futuretask的run方法。只不过futuretask封装了返回值以及一些超时方法。
五、线程池(略)
六、Runnable与Callable接口的区别
创建新类MyThread实现runnable接口
class MyThread implements Runnable{
@Override
public void run() {
}
}
新类MyThread2实现callable接口
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 200;
}
}
面试题:runnable接口与callable接口的区别?
- 落地方法不一样:一个是run,一个是call
- 是否有返回值:前者run()方法无返回值,后者call()方法有返回值
- 是否抛异常:前者没有异常,后者需要处理异常
七、new Thread()与Runnable接口建立线程的区别
Thread本来就是实现了Runnable,包含Runnable的功能是很正常的啊!!至于两者的真正区别最主要的就是一个是继承,一个是实现;其他还有一些面向对象的思想,Runnable就相当于一个作业,而Thread才是真正的处理线程,我们需要的只是定义这个作业,然后将作业交给线程去处理,这样就达到了松耦合,也符合面向对象里面组合的使用,另外也节省了函数开销,继承Thread的同时,不仅拥有了作业的方法run(),还继承了其他所有的方法。综合来看,用Runnable比Thread好的多。
runnable接口的作用本身是一种策略模式的体现。
八、FutureTask
FutureTask的异步调用
FutureTask,用它就干一件事,异步调用。
main方法就像一个冰糖葫芦,一个个方法由main串起来。但解决不了一个问题:正常调用挂起堵塞问题。
例子:
(1)老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单起个线程找班长帮忙买水,
水买回来了放桌上,我需要的时候再去get。
(2)4个同学,A算1+20,B算21+30,C算31*到40,D算41+50,是不是C的计算量有点大啊,
FutureTask单起个线程给C计算,我先汇总ABD,最后等C计算完了再汇总C,拿到最终结果
(3)高考:会做的先做,不会的放在后面做
FutureTask原理
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。因此get方法常放到最后。
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
注意:
只计算一次,缓存上次结果
get方法放到最后,否则会阻塞主线程的执行,影响效率
代码
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
class MyThread implements Runnable{
@Override
public void run() {
}
}
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"come in callable");
return 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws Exception {
//FutureTask<Integer> futureTask = new FutureTask(new MyThread2());
FutureTask<Integer> futureTask = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 1024;
});
FutureTask<Integer> futureTask2 = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 2048;
});
new Thread(futureTask,"zhang3").start();
new Thread(futureTask2,"li4").start();
//System.out.println(futureTask.get());
//System.out.println(futureTask2.get());
//1、一般放在程序后面,直接获取结果
//2、只会计算结果一次
while(!futureTask.isDone()){
System.out.println("***wait");
}
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName()+" come over");
}
}
/**
*
*
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,
当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,
就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,
然后会返回结果或者抛出异常。
只计算一次
get方法放到最后
*/
JUC强大的辅助类讲解
CountDownLatch减少计数
Count-计数 Down-向下 Latch-门闩
示例
package com.atguigu.thread;
import java.util.concurrent.CountDownLatch;
/**
*
* @Description:
* *让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。
*
* CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
* 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
* 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
*
* 解释:6个同学陆续离开教室后值班同学才可以关门。
*
* main主线程必须要等前面6个线程完成全部工作后,自己才能开干
*/
public class CountDownLatchDemo
{
public static void main(String[] args) throws InterruptedException
{
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6; i++) //6个上自习的同学,各自离开教室的时间不一致
{
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t 号同学离开教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t****** 班长关门走人,main线程是班长");
}
}
原理
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
- 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
CyclicBarrier循环栅栏
示例
package com.atguigu.thread;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
*
*
* CyclicBarrier
* 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,
* 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
* 直到最后一个线程到达屏障时,屏障才会开门,所有
* 被屏障拦截的线程才会继续干活。
* 线程进入屏障通过CyclicBarrier的await()方法。
*
* 集齐7颗龙珠就可以召唤神龙
*/
public class CyclicBarrierDemo
{
private static final int NUMBER = 7;
public static void main(String[] args)
{
//API:CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, ()->{System.out.println("*****集齐7颗龙珠就可以召唤神龙");}) ;
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+"\t 星龙珠被收集 ");
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
原理
- CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
- 线程进入屏障通过CyclicBarrier的await()方法。
- CyclicBarrier用的是加法计数,CountDownLatch用的是减法计数,但都是在最后完成功能
Semaphore信号灯
示例
package com.atguigu.thread;
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
*
* @Description: TODO(这里用一句话描述这个类的作用)
*
* 在信号量上我们定义两种操作:
* acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),
* 要么一直等下去,直到有线程释放信号量,或超时。
* release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
*
* 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
*/
public class SemaphoreDemo
{
public static void main(String[] args)
{
Semaphore semaphore = new Semaphore(3);//模拟3个停车位
for (int i = 1; i <=6; i++) //模拟6部汽车
{
new Thread(() -> {
try
{
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t 抢到了车位");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+"\t------- 离开");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
原理
在信号量上我们定义两种操作:
- acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
- release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
- 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
ReentrantReadWriteLock()读写锁
原理
读读可共享,写读写写要独占
之前学习过Lock锁,无论是读还是写操作,都要独占,这样影响效率。因此,提出读写锁。
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。即,只要有写就不能共存。
总结:
读-读能共存
读-写不能共存
写-写不能共存
代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
rwLock.writeLock().lock();//写锁
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写" + key);
//暂停一会儿线程
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写完了" + key);
System.out.println();
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
public Object get(String key) {
rwLock.readLock().lock();//读锁,防止读的时候其他线程来写
Object result = null;
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读完了" + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.put(num + "", num + "");
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.get(num + "");
}, String.valueOf(i)).start();
}
}
}
BlockingQueue阻塞队列
原理
阻塞:必须要阻塞/不得不阻塞
阻塞队列是一个队列,在数据结构中起的作用如下图:
线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
用处
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
架构介绍
种类分析
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。(重要)
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。(重要)
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。(重要)
- LinkedTransferQueue:由链表组成的无界阻塞队列。
- LinkedBlockingDeque:由链表组成的双向阻塞队列。
核心方法
代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 阻塞队列
*/
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// List list = new ArrayList();
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//第一组
// System.out.println(blockingQueue.add("a"));
// System.out.println(blockingQueue.add("b"));
// System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.element());
//System.out.println(blockingQueue.add("x"));
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// 第二组
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("b"));
// System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("x"));
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// 第三组
// blockingQueue.put("a");
// blockingQueue.put("b");
// blockingQueue.put("c");
// //blockingQueue.put("x");
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// 第四组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));
}
}
常见面试题
public class TestTransferValue {
public void changeValue1(int age){
age = 30;
}
public void changeValue2(Person person){
person.setPersonName("xxx");
}
public void changeValue3(String str){
str = "xxx";
}
public static void main(String[] args) {
TestTransferValue test = new TestTransferValue();
int age = 20;
test.changeValue1(age);
System.out.println("age----"+age);
Person person = new Person("abc");
test.changeValue2(person);
System.out.println("personName-------"+person.getPersonName());
String str = "abc";
test.changeValue3(str);
System.out.println("String-----"+str);
}
}
总结:
- 如果传参是普通类型,那么值不会变;
- 如果传参是自定义的引用类型,那么值会变;
- 如果传参是JDK自带的引用类型,那么值不会变;
ThreadPool线程池
例子:
10年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球,CPU需要来回切换。
现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
线程池的优势:
线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用;控制最大并发数;管理线程。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的使用
架构说明
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
编码实现
- Executors.newFixedThreadPool(int):执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程
- Executors.newSingleThreadExecutor():一个任务一个任务的执行,一池一线程
- Executors.newCachedThreadPool():执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池:ExecutorService
* 线程池的工具类:Executors
* Arrays
* Collections
* Executors
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
//List list = new ArrayList();
//List list = Arrays.asList("a","b");
//固定数的线程池,一池五线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5); //一池5个工作线程,类似一个银行网点,5个受理业务的窗口
// ExecutorService threadPool = Executors.newSingleThreadExecutor(); //一池1个工作线程,类似一个银行网点,1个受理业务的窗口
ExecutorService threadPool = Executors.newCachedThreadPool(); //一池N个工作线程,类似一个银行网点,可扩展N个受理业务的窗口
//10个顾客请求
try {
for (int i = 1; i <=10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
ThreadPoolExecutor底层原理
线程池7个重要参数
- corePoolSize:线程池中的常驻核心线程数
- maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
- keepAliveTime:多余的空闲线程的存活时间。当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
- handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略(窗口满了,候客区也满了)
线程池底层工作原理
-
在创建了线程池后,开始等待请求。
-
当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。 -
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
线程池用哪个?生产中如何设置合理参数
线程池的拒绝策略
等待队列已经排满了,再也塞不下新任务了。同时,线程池中的max线程也达到了,无法继续为新任务服务。这个时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。(回退)
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
以上内置拒绝策略均实现了RejectedExecutionHandle接口。
在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?(超级大坑)
答案是一个都不用,我们工作中只能使用自定义的。
Executors中JDK已经给你提供了,为什么不用?
在工作中如何使用线程池,是否自定义过线程池
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
/**
* 线程池
* Arrays
* Collections
* Executors
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(3),// LinkedBlockingQueue
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()
//new ThreadPoolExecutor.CallerRunsPolicy()
//new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardOldestPolicy()
);
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
private static void threadPool() {
//List list = new ArrayList();
//List list = Arrays.asList("a","b");
//固定数的线程池,一池五线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5); //一个银行网点,5个受理业务的窗口
// ExecutorService threadPool = Executors.newSingleThreadExecutor(); //一个银行网点,1个受理业务的窗口
ExecutorService threadPool = Executors.newCachedThreadPool(); //一个银行网点,可扩展受理业务的窗口
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
最大线程数要看电脑是cpu密集型还是IO密集型。
如果是cpu密集型,其最大线程数一般比cpu核数多1~2;
如果是IO密集型,其最大线程数一般是cpu核数/(1-阻塞系数)。
Java8之流式计算复习
函数式接口
java内置核心四大函数式接口
函数式接口都是单方法的,因此都可以用Lambda表达式表示(将原始创建匿名内部类的方法转化为Lambda表达式,更简洁)
//R apply(T t);函数型接口,一个参数,一个返回值
Function<String,Integer> function = t ->{return t.length();};
System.out.println(function.apply("abcd"));
//boolean test(T t);断定型接口,一个参数,返回boolean
Predicate<String> predicate = t->{return t.startsWith("a");};
System.out.println(predicate.test("a"));
// void accept(T t);消费型接口,一个参数,没有返回值
Consumer<String> consumer = t->{
System.out.println(t);
};
consumer.accept("javaXXXX");
//T get(); 供给型接口,无参数,有返回值
Supplier<String> supplier =()->{return UUID.randomUUID().toString();};
System.out.println(supplier.get());
//Lambda表达式只有一个方法时,可以省略参数类型,又因为只有一个参数,所以也可以省略括号
Stream流
流(Stream)是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是 数据,流讲的是计算!”
- Stream 自己不会存储元素
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
流的使用
- 创建一个Stream:一个数据源(数组、集合)
- 中间操作:一个中间操作,处理数据源数据
- 终止操作:一个终止操作,执行中间操作链,产生结果
源头=>中间流水线=>结果
package com.atguigu.juc.study;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
class User
{
private Integer id;
private String userName;
private int age;
}
/**
* @create 2019-02-26 22:24
*
* 题目:请按照给出数据,找出同时满足
* 偶数ID且年龄大于24且用户名转为大写且用户名字母倒排序
* 最后只输出一个用户名字
*/
public class StreamDemo
{
public static void main(String[] args)
{
User u1 = new User(11,"a",23);
User u2 = new User(12,"b",24);
User u3 = new User(13,"c",22);
User u4 = new User(14,"d",28);
User u5 = new User(16,"e",26);
List<User> list = Arrays.asList(u1,u2,u3,u4,u5);
list.stream().filter(p -> {
return p.getId() % 2 == 0;
}).filter(p -> {
return p.getAge() > 24;
}).map(f -> {
return f.getUserName().toUpperCase();
}).sorted((o1, o2) -> {
return o2.compareTo(o1);
}).limit(1).forEach(System.out::println);
// R apply(T t);
Function<String,Integer> function = t -> {return t.length();};
System.out.println(function.apply("abc"));
// boolean test(T t);
Predicate<String> predicate = t -> {return t.startsWith("a");};
System.out.println(predicate.test("a"));
//void accept(T t);
Consumer<String> consumer = t -> {System.out.println(t);};
consumer.accept("java1018");
// T get();
Supplier<String> supplier = () -> {return UUID.randomUUID().toString();};
System.out.println(supplier.get());;
}
}
分支合并框架
Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并
相关类
-
ForkJoinPool
分支合并池 类比=> 线程池 -
ForkJoinTask
ForkJoinTask 类比=> FutureTask
我们使用Fork拆分,再使用join合并时,是多个线程在执行的,且是有返回值的,因此我们要使用Callable接口(这里不能使用Runnable接口,因为其没有返回值,而Callable接口有返回值)。
这里ForkJoinTask实现了Future接口,实现了Future接口那么就可以使用Callable接口(做形参),因此,我们使用ForkJoinTask 。
- RecursiveTask
递归任务:继承后可以实现递归(自己调自己)调用的任务
class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) { this.n = n; }
Integer compute() {
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}
异步回调
import java.util.concurrent.CompletableFuture;
public class CompletableFutureDemo {
public static void main(String[] args) throws Exception {
//同步,异步,异步回调
//同步
// CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
// System.out.println(Thread.currentThread().getName()+"\t completableFuture1");
// });
// completableFuture1.get();
//异步回调
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"\t completableFuture2");
int i = 10/0;
return 1024;
});
System.out.println(completableFuture2.whenComplete((t,u)->{//正常执行
System.out.println("-------t="+t);//返回的数据
System.out.println("-------u="+u);//返回异常,如果正常执行,即为null,否则即为异常类型
}).exceptionally(f->{//出现异常
System.out.println("-----exception:"+f.getMessage());//异常时,返回异常类型
return 444;
}).get());//根据返回值为1024还是444即可判断程序是否发生异常
}
}
正确结果
异常结果