通俗易懂的并发编程-基础

前言

好久都没写博客了,种种原因,感觉快荒废了,刚好最近在总结并发编程的知识,所以就用博客记录一下,主要还是为了加强自己的记忆和理解。

启动方式

1、Thread

X extend Thread 然后x.start();

2、Runnable

X implements Runnanle 然后把x交给Thread

分析源码发现启动一个线程只有这两个方法,可能会有人有疑问了,那callable不是也可以吗,确实我们在其他地方搜索都会说3中,其实我认为那是错误的,来上源码证明(Thread类的源码)

<p>
// 有两种方法可以创建新的执行线程、一种是申明类是thread的子类
* There are two ways to create a new thread of execution. One is to
* declare a class to be a subclass of <code>Thread</code>. This
* subclass should override the <code>run</code> method of class
* <code>Thread</code>. An instance of the subclass can then be
* allocated and started. For example, a thread that computes primes
* larger than a stated value could be written as follows:
* <hr><blockquote><pre>
*     class PrimeThread extends Thread {
*         long minPrime;
*         PrimeThread(long minPrime) {
*             this.minPrime = minPrime;
*         }
*
*         public void run() {
*             // compute primes larger than minPrime
*             &nbsp;.&nbsp;.&nbsp;.
*         }
*     }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
*     PrimeThread p = new PrimeThread(143);
*     p.start();
* </pre></blockquote>
* <p>
//创建线程的另一种方法是声明一个类实现Runnable接口
* The other way to create a thread is to declare a class that
* implements the <code>Runnable</code> interface. That class then
* implements the <code>run</code> method. An instance of the class can
* then be allocated, passed as an argument when creating
* <code>Thread</code>, and started. The same example in this other
* style looks like the following:
* <hr><blockquote><pre>
*     class PrimeRun implements Runnable {
*         long minPrime;
*         PrimeRun(long minPrime) {
*             this.minPrime = minPrime;
*         }
*
*         public void run() {
*             // compute primes larger than minPrime
*             &nbsp;.&nbsp;.&nbsp;.
*         }
*     }
* </pre></blockquote><hr>
* <p>

callable

callable是一个任务是可以实现带返回值的任务,虽然callable不是一个启动线程的方式但是还是不自觉的想分析一下,因为在多线程的编程中使用的还是很频繁的

  • Callable- 构建带返回值的任务

    Callable是一个泛型接口,要比Runnable强些 ,因为接口方法call有返回值,并且返回值是传入的泛型类型,还能call的过程中抛出异常。

    public interface Callable<V>{
      V call() throws Exception;
    }
    
  • Future- 和异步线程通信的接口

    Future是一个泛型接口类,是Runnable和Callable的调度容器,就是对Runnable和Callable的结果进行操作,比如:

    1、isCancelled()取消操作,call方法任务完成前取消,返回true。

    2、isDone()判断是否操作完成,是则返回true。

    3、get()获取操作结果,会导致程序阻塞,必须等到子线程结束才会得到返回值。

    4、get(long timeout TimeUnit unit)在某时间后获取操作结果,如果在规定时间内获取不到返回值将会抛出超时异常)

    public interface Future<V>{
      boolean isCancelled();
      boolean isDone();
      V get() throws InterputeredException,ExecutionExeception;
      V get(long timeout ,TimeUnit unit)throws InterputeredException,ExecutionExeception,TimeoutExeception;
      
    }
    
    
    实例:
    ExecutorService threadPool=Executors.newSingleThreadExecutor();
    Future future=threadPool.submit(new Callable<String>(){
      public String call()throws Exception{
        return "result";
      }
      
    });
    
    future.get();//返回操作结果
    
  • FutureTask- Future 的唯一实现类,同时实现了Runnable类,通过 get() 可以拿到结果,但是阻塞当前线程

    FutureTask类同时实现了Runnable和Future 两个接口,具有了两个接口的属性。

    public class FutureTask<V> implements RunnableFuture<V>{
      public FutureTask(Callable<V> callable){
           this.callable=callable;
           ...
         }
      public FutureTask(Runnable runnable,V result){
        this.callable=Executors.callable(runnable,result);
        ...
      }
      boolean isCancelled();
      boolean isDone();
      V get() throws InterputeredException,ExecutionExeception;
      V get(long timeout ,TimeUnit unit)throws InterputeredException,ExecutionExeception,TimeoutExeception;
    }
    public interface RunnableFuture<V> implements Runnable,Future<V>{
      void run();
    }
    }
    

    这里的FutureTask间接实现了两个接口,在FutureTask的构造方法传入Callable或者是Runnable,而Runnable会转为Callable,runnable通过runnableadapter转为callable。同时FutureTask还具备Future的所有方法。

    实例:
    Callable<Integer> callable=new Callable<Integer>(){
      public Ingeter call() throws Exception{
        return 100;
      }
    };
    FutureTask<Integer> task=new FutureTask<Integer>(callable);
    new Thread(task).start();
    
    task.get();//当然要先启动线程才能得到结果;
    

    这里的callable当成runnable用了。

    总结:

    1. Callable比Runnable高级能返回结果值和抛出异常
    2. 可以有上述例子看到Callable来产生结果,Futuretask来获取结果。
    3. 在获取结果期间还可以控制是否取消thread 判断thread是否完成。
    4. FutureTask调用get方法获取call返回值过程中,主线程会阻塞,直到call方法结束并且返回。

线程状态

Java中线程状态分7个

1、初始状态(new)

当程序使用new 关键字创建了一个线程之后,该线程就处于新建状态,还没有调用start方法.
处于新建状态,和其他Java对象一样,仅仅由虚拟机为其分配内存,并初始化成员变量的值,不会有任何线程的动态特征。

2、就绪状态(start)

线程调用start()方法后,该线程就处于就绪状态。
线程处于就绪状态并不会立即进入运行状态,至于程序何时开始运行,取决于JVM线程调度器的调度。
就绪和运行状态之间的转换不受程序控制,由系统线程调度所决定,就绪状态获得处理器资源后才进入运行状态。
如果在主线程里希望调用子线程的start()方法后子线程立即进入运行状态,可以使用主线程的Thread.sleep(1)睡眠1毫秒,在主线程阻塞1秒期间,CPU会去执行另一个处于就绪状态的线程,这样相当于子线程立即进入运行状态。

注意:只能对处于新建状态的线程调用start()方法,不然会报IllegalThreadStateException异常

3、运行状态(run)

如果**处于就绪状态的线程获得了CPU,开始执行run()方法或call()方法的线程执行体,**则该线程处于运行状态。
一个线程开始运行后,不可能一直处于运行状态(除非它的线程执行体时间足够短,瞬间就执行结束),线程在运行过程中需要被中断

4、等待状态(wait)

进入该状态的线程需要等待其他线程作出一些特定动作(通知或中断)

5、超时等待状态(timed-wait)

该状态不同于waiting,他可以在指定的时间返回

6、阻塞状态(blocking)

线程在运行中被中断,使其他线程获得执行的机会,这个时候称为阻塞。
阻塞解除——>就绪状态——>运行状态

常见被阻塞并以及解除阻塞重新进入就绪状态的方法:
  • 调用sleep()方法
    调用sleep()方法的线程经过了指定时间。
  • 调用阻塞式IO方法,在该方法返回之前,该线程被阻塞
    线程调用的阻塞式IO方法已经返回。
  • 线程试图获得一个同步监视器,但该同步监视器被其他线程所持有
    线程成功获得了试图取得的同步监视器。
  • 线程在等待某个通知(notify)
    线程正在等待某个通知时,其他线程发出了一个通知。

注意sleep()方法和yield()方法,yield()方法不是阻塞,而是直接进入就绪状态。

7、终止状态(terminated)

  • run()或call()方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的Exception或Error。

调用线程对象的isAlive()方法可判断线程是否死亡,线程处于就绪,运行,阻塞三种状态时返回true,处于新建和死亡时,返回false

注意:已经死亡的线程不能调用start()方法,会报IllegalThreadStateException异常,即已死亡的线程无法再次运行。
只能对新建状态的线程调用start()方法

死锁

概念

通熟概念

两个或两个以上的线程在执行过程中,由于竞争资源或彼此通信而造成的一种阻塞现象,若无外力作用,他们都将无法进行下去,此时系统处于死锁状态或系统产生了死锁.

学术概念

死锁的发生必须具备一下4个必要条件

1、互斥条件:指线程对所分配的资源进行排他性使用,即在一段时间内某资源由一个线程占用,如果此时还有其他线程请求资源,则请求者只能等待直到占有资源的进行用毕并释放该资源.

2、请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程阻塞,但又对自己已经获得的其他资源保持不放.

3、不剥夺条件:指线程已获得的资源,在未使用完之前,不能被剥夺,自能在使用完毕后由自己释放.

4、环路等待条件:指发生死锁时,必然存在一个线程–资源环形链

可以通过打破四个必要条件来避免死锁的产生

1、打破互斥条件:改造独占性资源为虚拟资源(大部分资源已经无法改造)

2、打破请求和保持条件:当一个线程占有独占性资源之后又申请一独占性资源而无法满足时,则推出原来占有的资源

3、打破不剥夺条件:采用资源预先分配的策略,即线程运行前就申请全部资源,满足则运行,不满足就等待,这样就不会占有且申请.

4、打破环路等待条件:采用资源预先分配的策略,对所有设备实施分类编号,所有线程只能采用按序号递增的方式申请资源

危害

1、线程不工作了,但是整个程序及进程还是活的

2、没有任何异常信息供我们检查

3、一旦程序发生了死锁,是没有任何办法恢复的,只能重启程序,对线上程序来说这就是一个很严重的事件.

解决方法

关键是保证拿锁的顺序一致

两种解决方式

1、内部通过顺序比较,确保拿锁的顺序

2、采用尝试那锁的机制(显示锁tryLock)

线程安全

线程安全的本质可以理解为 线程同步

Java 提供了一系列的关键字和类来保证线程安全

1、Synchronized 关键字

作用
1). 保证方法或代码块操作的原子性

Synchronized 保证⽅法内部或代码块内部资源(数据)的互斥访问。即同⼀时间、由同⼀个 Monitor(监视锁) 监视的代码,最多只能有⼀个线程在访问。

2).保证监视资源的可见性

保证多线程环境下对监视资源的数据同步。即任何线程在获取到 Monitor 后的第⼀时间,会先将共享内存中的数据复制到⾃⼰的缓存中;任何线程在释放 Monitor 的第⼀时间,会先将缓存中的数据复制到共享内存中。

3).保证线程间操作的有序性

Synchronized 的原子性保证了由其描述的方法或代码操作具有有序性,同一时间只能由最多只能有一个线程访问,不会触发 JMM 指令重排机制。

2、Volatile 关键字

作用

保证被 Volatile 关键字描述变量的操作具有可见性有序性(禁止指令重排)

注意:
1.Volatile 只对基本类型 (byte、char、short、int、long、float、double、boolean) 的赋值 操作和对象的引⽤赋值操作有效。
2 对于 i++ 此类复合操作, Volatile 无法保证其有序性和原子性。
3.相对 Synchronized 来说 Volatile 更加轻量一些。

3、CAS

java.util.concurrent.atomic 包提供了一系列的 AtomicBooleanAtomicIntegerAtomicLong 等类。使用这些类来声明变量可以保证对其操作具有原子性来保证线程安全。

实现原理上与 Synchronized 使用 Monitor(监视锁)保证资源在多线程环境下阻塞互斥访问不同,java.util.concurrent.atomic 包下的各原子类基于 CAS(CompareAndSwap) 操作原理实现。

CAS 又称无锁操作,一种乐观锁策略,原理就是多线程环境下各线程访问共享变量不会加锁阻塞排队,线程不会被挂起。通俗来讲就是一直循环对比,如果有访问冲突则重试,直到没有冲突为止。

4、Lock

Lock 也是 java.util.concurrent 包下的一个接口,定义了一系列的锁操作方法。Lock 接口主要有 ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 实现类。与 Synchronized 不同是 Lock 提供了获取锁和释放锁等相关接口,使得使用上更加灵活,同时也可以做更加复杂的操作,有尝试拿锁的方法 ,如:

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
private int x = 0;
private void count() {
    writeLock.lock();
    try {
        x++;
    } finally {
        writeLock.unlock();
    }
}
private void print(int time) {
    readLock.lock();
    try {
        for (int i = 0; i < time; i++) {
            System.out.print(x + " ");
        }
        System.out.println();
    } finally {
        readLock.unlock();
    }
}

5、ThreadLocal

public class ThreadLocalDateUtil {
    private static final String date_format = "yyyy-MM-dd HH:mm:ss";
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
  
    public static DateFormat getDateFormat()  
    { 
        DateFormat df = threadLocal.get(); 
        if(df==null){ 
            df = new SimpleDateFormat(date_format); 
            threadLocal.set(df); 
        } 
        return df; 
    } 
    public static String formatDate(Date date) throws ParseException {
        return getDateFormat().format(date);
    }
    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }  
}

活锁

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断的发生同一个线程总是拿到同一把锁,在尝试拿另一把锁的时候,因为拿不到,而把已经持有的锁释放的过程就是活锁

线程饥饿

低优先级的线程总是拿不到执行时间,即拿不到操作系统调度的cpu资源

线程安全总结

1. 出现线程安全问题的原因:

在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写⼊入已经开始,但还没结束),其他线程对这个写了一半的资源进⾏了读操作,或者对这个写了一半的资源进⾏了写操作,导致此资源出现数据错误。

2. 如何避免线程安全问题?

  • 保证共享资源在同一时间只能由一个线程进行操作(原子性,有序性)。
  • 将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值