实现多线程的三种方法

 

  • 继承Thread类,重写父类run()方法

 

public class thread1 extends Thread {
    public void run() {
            for (int i = 0; i < 10000; i++) {
                    System.out.println("我是线程"+this.getId());
            }
    }
    public static void main(String[] args) {
            thread1 th1 = new thread1();
            thread1 th2 = new thread1();
            th1.start();
            th2.start();
    }
}

 

  • 实现runnable接口

 

public class thread2 implements Runnable {
    public String ThreadName;
    public thread2(String tName){
            ThreadName = tName;
    }
    public void run() {
            for (int i = 0; i < 10000; i++) {
                    System.out.println(ThreadName);
            }
    }
    public static void main(String[] args) {
            // 创建一个Runnable接口实现类的对象
            thread2 th1 = new thread2("线程A:");
            thread2 th2 = new thread2("线程B:");
            // 将此对象作为形参传递给Thread类的构造器中,创建Thread类的对象,此对象即为一个线程
            Thread myth1 = new Thread(th1);
            Thread myth2 = new Thread(th2);
            // 调用start()方法,启动线程并执行run()方法
            myth1.start();
            myth2.start();
    }
}

 

  • 通过Callable和Future创建线程

 

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableThreadTest implements Callable<Integer>
{
@Override
public Integer call() throws Exception{
    int i = 0;
    for(;i<100;i++){
        System.out.println(Thread.currentThread().getName()+" "+i);
    }
    return i;
}

public static void main(String[] args){
    CallableThreadTest ctt = new CallableThreadTest();
    FutureTask ft = new FutureTask<>(ctt);
    for(int i = 0;i < 100;i++){
        System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
        if(i==20){
            new Thread(ft,"有返回值的线程").start();
        }
    }
    try{
        System.out.println("子线程的返回值:"+ft.get());
    } catch (InterruptedException e){
        e.printStackTrace();
    } catch (ExecutionException e){
        e.printStackTrace();
    }
}
}

 

三种创建多线程方法的对比

 

1、采用实现Runnable、Callable接口的方式创建多线程时,线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。缺点是编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。 

 

2、使用继承Thread类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。缺点是线程类已经继承了Thread类,所以不能再继承其他父类。

 

3、Runnable和Callable的区别 

(1) Callable规定重写call(),Runnable重写run()。 

(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。

(3) call方法可以抛出异常,run方法不可以。

(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

 

线程池

 

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池的产生和数据库的连接池类似,系统启动一个线程的代价是比较高昂的,如果在程序启动的时候就初始化一定数量的线程,放入线程池中,在需要是使用时从池子中去,用完再放回池子里,这样能大大的提高程序性能,再者,线程池的一些初始化配置,也可以有效的控制系统并发的数量,防止因为消耗过多的内存,而把服务器累趴下。

 

通过Executors工具类可以创建各种类型的线程池,如下为常见的四种:

 

<1>理解线程池,先从阻塞队列学习

 

入队:

非阻塞队列:当队列中满了的时候,放入数据,数据丢失

阻塞队列:当队列满了的时候,进行等待,什么时候队列中有出队的数据,那么新数据再插入进去

 

出队:

非阻塞队列:如果现在队列中没有元素,取数据,得到的是null

阻塞队列:等待,什么时候放进去,再把数据取出来

 

线程池用的是阻塞队列,用到的重要类是如下类

 

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

 

 

几种常见的线程池都是通过Executors生成的,有四种,如下所示

 

可缓存线程池  newCachedThreadPool :大小不受限,当线程释放时,可重用该线程;

 

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue(),
                                  threadFactory);
}

 

 

定长线程池(核心线程数与最大线程数相等) newFixedThreadPool :大小固定,无可用线程时,任务需等待,直到有可用线程;

 

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue());
}

 

 

定时线程池  newSingleThreadExecutor :创建一个单线程,任务会按顺序依次执行;

 

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

 

 

单例线程池:(核心线程数与最大线程数都为1) newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行

 

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue()));
}

使用线程池的好处

 

  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
  • 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现
     

线程池都有哪几种工作队列

 

1、ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

 

 2、LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列 

 

3、SynchronousQueue 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。 

 

4、PriorityBlockingQueue 一个具有优先级的无限阻塞队列。

 

什么是乐观锁和悲观锁

 

(1)乐观锁:很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会去判断在此期间有没有人去更新这个数据(可以使用版本号等机制)。如果因为冲突失败就重试。乐观锁适用于写比较少的情况下,即冲突比较少发生,这样可以省去了锁的开销,加大了系统的整个吞吐量。像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。 

 

(2)悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,因此每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁,效率比较低。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

 

乐观锁的实现方式(CAS)

 

乐观锁的实现主要就两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 Compare and Swap ( CAS )。CAS:CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

 

乐观锁是一种思想,CAS是这种思想的一种实现方式。

CAS的缺点

 

  1. ABA问题
     

如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。ava并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

这样说或许有些抽象,我们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。

java 多线程加入队列 java多线程队列的使用_阻塞队列

image

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

java 多线程加入队列 java多线程队列的使用_乐观锁_02

image

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

java 多线程加入队列 java多线程队列的使用_乐观锁_03

image

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

java 多线程加入队列 java多线程队列的使用_乐观锁_04

4.jpg

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

java 多线程加入队列 java多线程队列的使用_阻塞队列_05

image

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

java 多线程加入队列 java多线程队列的使用_线程池_06

image

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

java 多线程加入队列 java多线程队列的使用_java 多线程加入队列_07

image

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS的缺点:

1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

作者:Flagle
链接:https://www.jianshu.com/p/ae25eb3cfb5d
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 

 

  1. 循环时间长开销很大
     

自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

 

  1. 只能保证一个共享变量的原子操作。
     

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。