JAVA多线程——(二)多线程编程


文章目录

  • JAVA多线程——(二)多线程编程
  • 【一】ReentrantLock
  • 【二】ReadWriteLock
  • 【三】Condition
  • 【四】并发容器
  • 【五】Atomic
  • 【六】ExecutorService
  • 【七】CountDownLatch
  • 【八】CyclicBarrier
  • 【九】Volatile
  • 【十】ThreadLocal


【一】ReentrantLock

虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

  • lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
  • tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待
  • tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
  • lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
  • 我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:
    ReentrantLock lock = new ReentrantLock(true);
public class Main {
     Lock lock = new ReentrantLock();
     AtomicInteger integer = new AtomicInteger(1);

    public void print(){
        //使用ReentrantLock加锁的时候,必须在finally中释放锁,不然可能造成死锁
        lock.lock();
        try {
            System.out.println(integer.get());
            integer.getAndIncrement();
        }finally {
            lock.unlock();
        }

    }

    public static void main(String args[]){
        Main main = new Main();

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.print();
                
            }
        });

        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.print();
            }
        });

        thread1.start();
        thread2.start();
    }
}

【二】ReadWriteLock

读写锁是一种通用技术,在其他编程语言或者数据库中都有对应实现。读写锁一般遵守下面三条规则:

  • 允许多个线程获取读锁
  • 只允许一个线程获取写锁
  • 如果某个线程获取了写锁,其他线程不能再获取读锁
    由于规则1多个线程在只读的情况下可以同时读取数据获取共享变量,所以读写锁优于互斥锁。

读写锁适用于读多写少:

ReadWriteLock接口的两个方法:

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();
}

例子:

public class Main {

    ReadWriteLock lock = new ReentrantReadWriteLock();
    AtomicInteger count = new AtomicInteger(1);

    public void get(){
        //获取数据时,获取读锁
        Lock readLock = this.lock.readLock();
        readLock.lock();
        try {
            System.out.println(count);
        }finally {
            readLock.unlock();
        }
    }

    public void add(){
        //写的时候,获取写锁
        Lock writeLock = this.lock.writeLock();
        writeLock.lock();
        try {
            count.getAndIncrement();
        }finally {
            writeLock.unlock();
        }
    }

    public static void main(String args[]){
        Main main = new Main();

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.get();
            }
        });

        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.add();
            }
        });

        thread1.start();
        thread2.start();
    }
}

【三】Condition

在java Lock体系下依然会有同样的方法实现等待/通知机制,而Condition与Lock配合完成等待通知机制。

  • Condition能够支持不响应中断,而通过使用Object方式不支持;
  • Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  • Condition能够支持超时时间的设置,而Object不支持


await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
signal()All :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

例子:

public class Main {
    public Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public Stack<Integer> stack = new Stack();

    public int i = 1;

    public void producer(){
        lock.lock();
        try {
            while (stack.isEmpty()){
                stack.push(new Integer(i++));
                condition.signalAll();
                condition.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void consumer(){
        lock.lock();
        try {
            while (!stack.isEmpty()){
                System.out.println(stack.pop());
                condition.signalAll();
                condition.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public static void main(String args[]){
        Main main = new Main();
        Thread thread1 = new Thread(()->{
            main.producer();
        });

        Thread thread2 = new Thread(()->{
            main.consumer();
        });

        thread1.start();
        thread2.start();
    }
}

【四】并发容器

  • ConcurrentHashMap
    主要为了解决HashMap线程不安全和Hashtable效率不高的问题。
    1、JDK7版本
    分段锁机制,简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable
    ConcurrentHashMap的数据结构:

    ConcurrentHashMap类结构如上图所示。由图可知,在ConcurrentHashMap中,定义了一个Segment<K, V>[]数组来将Hash表实现分段存储,从而实现分段加锁;而么一个Segment元素则与HashMap结构类似,其包含了一个HashEntry数组,用来存储Key/Value对。Segment继承了ReetrantLock,表示Segment是一个可重入锁,因此ConcurrentHashMap通过可重入锁对每个分段进行加锁

    2、JDK8版本
    在JDK1.8中,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现

    CAS(Compare And Swap,比较交换):CAS有三个操作数,内存值V、预期值A、要修改的新值B,当且仅当A和V相等时才会将V修改为B,否则什么都不做。

    JDK1.8的数据结构:
public class Main {
    private ConcurrentHashMap<String,Character> map = new ConcurrentHashMap<>();

    public void producer(String key,char value){
        map.put(key,value);
    }

    public void consumer(String key){
        Character value = map.get(key);
        System.out.println(value);
    }


    public static void main(String args[]){
        Main main = new Main();
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.producer(String.valueOf(i), (char) ('a'+i));
            }
        });

        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.consumer(String.valueOf(i));
            }
        });

        thread1.start();
        thread2.start();
    }
}
  • ConcurrentLinkedQueue
    ConcurrentLinkedQueue是一个基于链表的无界非阻塞队列,并且是线程安全的,它采用的是先进先出的规则,当我们增加一个元素时,它会添加到队列的末尾,当我们取一个元素时,它会返回一个队列头部的元素。
offer():将指定的元素插入队列的尾部
poll() :获取并移除队列的头,如果队列为空则返回null
peek():获取表头元素但不移除队列的头,如果队列为空则返回null。

remove(Object obj):移除队列已存在的元素,返回true,
如果元素不存在,返回false。

add(E e):将指定元素插入队列末尾,成功返回true,
失败返回false(此方法非线程安全的方法,不推荐使用)。

注意:

虽然ConcurrentLinkedQueue的性能很好,
但是在调用size()方法的时候,会遍历一遍集合
对性能损害较大,执行很慢,因此应该尽量的减少使用这个方法,
如果判断是否为空,最好用isEmpty()方法。

ConcurrentLinkedQueue不允许插入null元素,会抛出空指针异常。

ConcurrentLinkedQueue是无界的,所以使用时,
一定要注意内存溢出的问题。即对并发不是很大中等的情况下使用,
不然占用内存过多或者溢出,对程序的性能影响很大,甚至是致命的。
  • CopyOnWriteArrayList
1、实现了List接口

2、内部持有一个ReentrantLock lock = new ReentrantLock();

3、底层是用volatile transient声明的数组 array

4、读写分离,写时复制出一个新的数组,完成插入、
修改或者移除操作后将新数组赋值给array

性能上:
Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降。

而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况

  • CopyOnWriteArraySet
  • BlockingQueue
  • java多线程阻塞线程的方式有哪些 java多线程锁的方式有哪些?_i++

【五】Atomic

  • 线程安全的几个问题:
    1、原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作
    2、可见性:一个线程对主内存的修改可以及时的被其他线程观察到
    3、有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
  • 原子更新基本类型
AtomicBoolean: 原子更新布尔类型。 
AtomicInteger: 原子更新整型。 
AtomicLong: 原子更新长整型。
  • 原子更新数组
AtomicIntegerArray: 原子更新整型数组里的元素。 
AtomicLongArray: 原子更新长整型数组里的元素。 
AtomicReferenceArray: 原子更新引用类型数组里的元素。
  • 原子更新引用类型
AtomicReference: 原子更新引用类型。 
AtomicReferenceFieldUpdater: 原子更新引用类型的字段。 
AtomicMarkableReferce: 原子更新带有标记位的引用类型
  • 原子更新字段类
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。 
AtomicLongFieldUpdater: 原子更新长整型字段的更新器。 
AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。 
AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。

【六】ExecutorService

  • 一、线程池: 提供一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁的额外开销,提高了响应的速度。
  • 二、线程池的体系结构:
    java.util.concurrent.Executor 负责线程的使用和调度的根接口
    |–ExecutorService 子接口: 线程池的主要接口
    |–ThreadPoolExecutor 线程池的实现类
    |–ScheduledExceutorService 子接口: 负责线程的调度
    |–ScheduledThreadPoolExecutor : 继承ThreadPoolExecutor,实现了ScheduledExecutorService
  • 三、工具类 : Executors
    1、ExecutorService newFixedThreadPool() : 创建固定大小的线程池
    2、ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
    3、ExecutorService newSingleThreadExecutor() : 创建单个线程池。 线程池中只有一个线程
    4、ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务

【七】CountDownLatch

CountDownLatch,英文翻译为倒计时锁存器,是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

1、确保某个计算在其需要的所有资源都被初始化之后才继续执行;
2、确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
3、等待直到某个操作所有参与者都准备就绪再继续执行。

用法:

  • 第一种:
    某一线程在开始运行前等待n个线程执行完毕。
    将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
  • 第二种:
    实现多个线程开始执行任务的最大并行性。
    注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

例子:计算多线程耗时

public class TestCountDownLatch {

    public static void main(String[] args){
		//CountDownLatch 为唯一的、共享的资源
        final CountDownLatch latch = new CountDownLatch(5);
		
        LatchDemo latchDemo = new LatchDemo(latch);

        long begin = System.currentTimeMillis();

        for (int i = 0; i <5 ; i++) {
            new Thread(latchDemo).start();
        }
        try {
            //多线程运行结束前一直等待
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        
        System.out.println("耗费时间:"+(end-begin));

    }
}

class LatchDemo implements  Runnable{

    private CountDownLatch latch;

    public LatchDemo(CountDownLatch latch){
        this.latch=latch;
    }
    public LatchDemo(){
        super();
    }

    @Override
    public void run() {
        //当前对象唯一,使用当前对象加锁,避免多线程问题
        synchronized (this){
            try {
                for (int i = 0; i < 50000; i++) {
                    if (i%2==0){
                        System.out.println(i);
                    }
                }
            }finally {
                //保证肯定执行
                latch.countDown();
            }
        }
    }
}

【八】CyclicBarrier

CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

java多线程阻塞线程的方式有哪些 java多线程锁的方式有哪些?_i++_02


CyclicBarrier内部使用了ReentrantLock和Condition两个类。

public CyclicBarrier(int parties) {
    this(parties, null);
}
 
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

CyclicBarrier的另一个构造函数CyclicBarrier(int parties, Runnable barrierAction),用于线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。

public class CyclicBarrierTest {
	// 自定义工作线程
	private static class Worker extends Thread {
		private CyclicBarrier cyclicBarrier;
		
		public Worker(CyclicBarrier cyclicBarrier) {
			this.cyclicBarrier = cyclicBarrier;
		}
		
		@Override
		public void run() {
			super.run();
			
			try {
				System.out.println(Thread.currentThread().getName() + "开始等待其他线程");
				cyclicBarrier.await();
				System.out.println(Thread.currentThread().getName() + "开始执行");
				// 工作线程开始处理,这里用Thread.sleep()来模拟业务处理
				Thread.sleep(1000);
				System.out.println(Thread.currentThread().getName() + "执行完毕");
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
 
	public static void main(String[] args) {
	
		CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
		
		for (int i = 0; i < threadCount; i++) {
			System.out.println("创建工作线程" + i);
			Worker worker = new Worker(cyclicBarrier);
			worker.start();
		}
	}
}

【九】Volatile

【十】ThreadLocal