JAVA多线程并发

简介

进程是分配资源的基本单位,而线程是系统调度的基本单位,一个进程可以包含多个线程,这些线程共享进程的资源。

并发

并行与并发

  • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
  • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

并发的三个概念

1. 原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。可以通过加锁来实现。

2. 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。可以通过volatile来实现。

3. 有序性

即程序执行的顺序按照代码的先后顺序执行。

一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。在单线程中指令重排序并不会影响最后的结果,但是在多线程并发的情况下,可能会导致结果和串行的不一致。可以通过volatile来保证有序性,因为volatile加入了内存屏障防止指令重排序。

另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

 

线程的生命周期

新建和就绪

程序使用new会新建一个线程,new出的对象跟普通对象一样,JVM会为其分配内存,初始化成员变量等,此时线程并没有运行,而是就是新建状态。

当线程对象调用start后,线程将进入就绪状态。JVM会为其创建函数调度栈和计数器,但此时线程依然没有运行,而是等待获取CPU执行片。

运行和阻塞状态

当就绪状态的线程获取了CPU执行片的之后,就进入运行状态,但是在执行过程中,可能会因为以下原因使线程进入阻塞状态,

       ① 线程调用sleep()方法主动放弃所占用的处理器资源

  ② 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

  ③ 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将有深入的介绍

  ④ 线程在等待某个通知(notify)

  ⑤ 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法

线程从运行进入阻塞状态之后,接着只能继续阻塞或者再次进入就绪状态,下面情况会使线程由阻塞状态重新进入就绪状态,

       ① 调用sleep()方法的线程经过了指定时间。

  ② 线程调用的阻塞式IO方法已经返回。

  ③ 线程成功地获得了试图取得的同步监视器。

  ④ 线程正在等待某个通知时,其他线程发出了个通知。

  ⑤ 处于挂起状态的线程被调甩了resume()恢复方法。

死亡状态

       ① run()或call()方法执行完成,线程正常结束。

  ② 线程抛出一个未捕获的Exception或Error。

  ③ 直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

 

wait与sleep的差别

1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

2. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

监视器monitor

Java中的每个对象都有一个监视器,来监测并发代码的重入。为了实现监视器的互斥功能,每个对象(Object和class)都关联着一个锁(有时也叫“互斥量”),这个锁在操作系统书籍中称为“信号量”,互斥(“mutex”)是一个二进制的信号量 [每个对象同时只能被一个线程获取锁]。

JAVA使用wait()和notify()和notifyAll()方法实现线程的挂起和唤醒。此可以用来实现消费者和生产者模式。

public class Product_Consumer {

    public static void main(String[] args){
        Storage storage = new Storage();
        for(int i=0;i<20;i++){
            Thread producter = new Thread(new Runnable() {
                @Override
                public void run() {
                    storage.product();
                }
            });

            Thread consumer = new Thread(new Runnable() {
                @Override
                public void run() {
                    storage.consume();
                }
            });

            producter.start();
            consumer.start();
        }
    }

}

class Storage{
    public static final int MAX_STORAGE = 10;
    public static int nowStorage = 0;
    public synchronized void product(){
        if(nowStorage>=MAX_STORAGE){
            try {
                System.out.println("库存已满,此时库存为"+nowStorage);
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return;
        }
        nowStorage++;
        System.out.println(Thread.currentThread().getName()+"生产了一件产品,目前库存为"+nowStorage);
        notifyAll();
    }

    public synchronized void consume(){
        if (nowStorage<=0){
            try {
                System.out.println("库存为空,此时库存为"+nowStorage);
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return;
        }
        nowStorage--;
        System.out.println(Thread.currentThread().getName()+"消费了一件产品,目前库存为"+nowStorage);
        notifyAll();
    }
}

 

多线程创建

public class MyThread {
    public static void main(String[] args){
        //方法1:继承Thread
        for(int i=0;i<10;i++){
            ThreadTest threadTest = new ThreadTest();
            threadTest.start();
        }

        //方法2:实现Runnable接口
        for(int i=0;i<10;i++){
            RunnableTest runnableTest = new RunnableTest();
            Thread thread = new Thread(runnableTest);
            thread.start();
        }

        //方法3:实现Callable接口
        for(int i=0;i<10;i++){
            CallableTest callableTest = new CallableTest();
            FutureTask<String> futureTask = new FutureTask<String>(callableTest);
            Thread thread = new Thread(futureTask);
            thread.start();
            try {
                System.out.println(futureTask.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }


    }
}


class ThreadTest extends Thread{
    @Override
    public void run(){
        System.out.println("EXTENDS THREAD  "+this.getName());
    }
}

class RunnableTest implements Runnable{
    @Override
    public void run(){
        System.out.println("IMPLEMENTS RUNNABLE   "+Thread.currentThread().getName());
    }
}

class CallableTest implements Callable{
    @Override
    public String call() throws Exception {
        return "IMPLEMENTS CALLABLE  "+Thread.currentThread().getName();
    }
}

在java中多线程的创建有三种方式,分别为继承Thread,实现Runnable和Callable接口。

Callable的区别: Callable可以有返回值以及可以处理异常,Thread和Runnable不可以。

实现Runnable接口比继承Thread类所具有的优势

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

线程间的同步

synchronized 和 ReentrantLock

public class ThreadLock {
    public static int i = 10;
    public static final ReentrantLock reentrantLock = new ReentrantLock();

    public synchronized void synTest(){
        i++;
    }

    public void reenTrantTest(){
        try{
            reentrantLock.lock();
            i--;
        }finally {
            reentrantLock.unlock();
        }
    }

}

区别:

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锁适合代码少量的同步问题。

 

ThreadLocal

https://www.cnblogs.com/dolphin0520/p/3920407.html

public class ThreadLocalTest {
    public static void main(String[] args){
        System.out.println(MyThreadLocal.threadLocal.get());
        MyThreadLocal.threadLocal.set("hello world");
        System.out.println(MyThreadLocal.threadLocal.get());
        MyThreadLocal.threadLocal.remove();
    }
}

class MyThreadLocal{
    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
        @Override
        protected String initialValue(){
            return "x";
        }
    };
}

volatile

https://www.cnblogs.com/dolphin0520/p/3920373.html

因为数据是存储在主存中,CPU的处理速度远高于从主存中存取数据的速度,如果直接从主存中获取数据会非常的影响性能,因此CPU中会有一个高速缓存。

当多个CPU对同一数据进行处理时就会存在数据不一致的现象。

解决这种不一致的情况有以下两种方法:

1. 在总线上加锁,例如synchronized。

2. 缓存一致协议,即依靠volatile关键字来实现。

而volatile约定的内容有以下几点:

1. 当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

2. volatile修饰的数据在进行写操作后要立即写入主存。

3. 禁止指令重排序。

volatile的原理和实现机制

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile常见使用场景:单例模式双检锁,线程池状态

原子类

https://www.cnblogs.com/chengxiao/p/6789109.html

容器类

https://blog.csdn.net/sjjsh2/article/details/53286001

Iterator与for的区别:

https://blog.csdn.net/hufeng19940810/article/details/77933768

1. Iterator适合顺序访问(linkedlist),for适合随机访问(arraylist)

2. foreach的内部是依靠Iterator实现的

3. 如果在for中调用remove进行删除,会报越界错误,但是如果使用Iterator则不会,因为它的remove()方法不仅会删除元素,还会维护一个标志,用来记录目前是不是可删除状态,例如,你不能连续两次调用它的remove()方法,调用之前至少有一次next()方法的调用。

线程池

https://www.cnblogs.com/dolphin0520/p/3932921.html

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类

其构造函数有以下几个参数:

corePoolSize:核心池的大小,线程池刚创建时里面的线程数是为0的,当有任务需要处理时才开始创建线程,当线程数达到corePoolSize的时候,再有新任务需要处理时,则不再创建新的线程,而是将任务放在缓存队列中。

maximumPoolSize:线程池最大线程数。是线程池的一种补救措施,当任务量突然增大时,可以在corePoolSize的基础上增加线程。

keepAliveTime:当线程数超过corePoolSize的时候,keepAliveTime才起作用,当一个线程没有任务执行时间达到keepAliveTime时则进行销毁,直到线程数低于corePoolSize。

unit:keepAliveTime的单位。

workQueue:阻塞队列,一般使用LinkedBlockingQueue和Synchronous比较多。

threadFactory:线程工厂,用于创建线程的。

handler:拒绝处理任务时的策略有以下四个策略。

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

核心方法:

execute() :  通过此方法向线程池提供任务,交由线程池处理。

submit() : 这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果.

shutdown()和shutdownNow()是用来关闭线程池的。

还有很多其他的方法:

  比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法

 

线程池的状态

public class ThreadPool {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));

        for (int i = 0; i < 20; i++) {
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);
            System.out.println("线程池中总线程数:"+executor.getPoolSize());
            System.out.println("等待队列中任务数:"+executor.getQueue().size());
            System.out.println("已处理的任务数:"+executor.getCompletedTaskCount());
        }
        executor.shutdown();
    }
}

class MyTask implements Runnable {

    private int taskName;

    public MyTask(int taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println("正在执行task" + taskName);
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task" + taskName + "执行完毕");
    }
}

死锁

当有两个或更多的线程在等待对方释放锁并无限期地卡住时,这种情况就称为死锁。在java中出现嵌套的同步块,或者在同步块中企图锁定不同的对象时就容易发生死锁。

class DeadLockMthod{
    private String a = "xxx";
    private String b = "ccc";

    public void method1(){
        synchronized (a){
            System.out.println("method1 get a");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (b){
                System.out.println("method1 get b");
            }
        }
    }

    public void method2(){
        synchronized (b){
            System.out.println("method2 get b");
            synchronized (a){
                System.out.println("method2 get a");
            }
        }
    }
}

public class DeadLock{
    public static void main(String[] args) {
        DeadLockMthod deadLockMthod = new DeadLockMthod();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                deadLockMthod.method1();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                deadLockMthod.method2();
            }
        });

        threadA.start();
        threadB.start();
    }
}

产生死锁的四个必要条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁:

死锁的产生并不是因为多线程,而是因为请求锁的方式或者顺序不正确,则只要对锁进行一个合理的规划,实现一个有序的访问则可以避免死锁。

class DeadLockMthod{
    private String a = "xxx";
    private String b = "ccc";

    public void method1(){
        synchronized (b){
            System.out.println("method1 get b");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (a){
                System.out.println("method1 get a");
            }
        }
    }

    public void method2(){
        synchronized (b){
            System.out.println("method2 get b");
            synchronized (a){
                System.out.println("method2 get a");
            }
        }
    }
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值