Java多线程高并发编程代码笔记(三)

文章目录

    • 线程安全的单例模式
      • 多线程安全单例模式(不使用同步锁)
      • 多线程安全单例模式(使用同步方法)
      • 多线程安全单例模式(使用双重同步锁)
      • 多线程安全单例模式(延迟/懒加载 使用静态内部类)
      • 多线程安全单例模式(枚举实现)
    • 并发容器
      • 多线程卖票问题
        • 使用线程不安全的容器List
        • 使用线程安全的容器Vector
        • 在判断和操作放在同步代码块中
        • 使用队列(Queue)来实现
      • List、Map
        • ConcurrentHashMap和ConcurrentSkipListMap
        • CopyOnWriteArrayList 写时复制容器
        • Collections.synchronizedList
      • Queue
        • ConcurrentLinkedQueue 并发队列
        • BlockingQueue 阻塞式队列
          • LinkedBlockingQueue 无界阻塞式队列
          • ArrayBlockingQueue 有界阻塞式队列
          • DelayQueue 执行定时任务
          • TransferQueue
          • SynchronizedQueue
    • 参考
    • 源代码

线程安全的单例模式

参考:

设计模式之单例模式(线程安全)

Java单例模式:为什么我强烈推荐你用枚举来实现单例模式

多线程安全单例模式(不使用同步锁)

package demo28;

/**
 * 这种代码中的一个缺点是该类加载的时候就会直接new 一个静态对象出来,当系统中这样的类较多时,会使得启动速度变慢 。现在流行的设计都是讲“延迟加载”,我们可以在第一次使用的时候才初始化第一个该类对象。所以这种适合在小系统。
 */
public class Singleton1 {
    private static Singleton1 sin = new Singleton1();    ///直接初始化一个实例对象

    private Singleton1() {    ///private类型的构造函数,保证其他类对象不能直接new一个该对象的实例
    }

    public static Singleton1 getSin() {    ///该类唯一的一个public方法
        return sin;
    }

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println(Singleton1.getSin());
        new Thread(runnable,"t1").start();
        new Thread(runnable,"t2").start();
    }
}

运行结果

demo28.Singleton1@686e97cd
demo28.Singleton1@686e97cd

多线程安全单例模式(使用同步方法)

package demo28;

/**
 * 缺点:锁住了一个方法,锁的力度有点大
 */
public class Singleton2 {
    private static Singleton2 instance;
    private Singleton2(){

    }
    public static synchronized Singleton2 getInstance(){    //对获取实例的方法进行同步
        if (instance == null)
            instance = new Singleton2();
        return instance;
    }

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println(Singleton2.getInstance());
        new Thread(runnable,"t1").start();
        new Thread(runnable,"t2").start();
    }
}

运行结果

demo28.Singleton2@47f6725c
demo28.Singleton2@47f6725c

多线程安全单例模式(使用双重同步锁)

package demo28;

/**
 * 改进:不锁住整个方法,只锁住其中的new语句就OK。就是所谓的“双重锁”机制
 */
public class Singleton3 {
    private static Singleton3 instance;
    private Singleton3(){
    }
    public static Singleton3 getInstance(){    //对获取实例的方法进行同步
        if (instance == null){
            synchronized(Singleton3.class){
                if (instance == null)
                    instance = new Singleton3();
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println(Singleton3.getInstance());
        new Thread(runnable,"t1").start();
        new Thread(runnable,"t2").start();
    }

}

运行结果

demo28.Singleton3@76575e50
demo28.Singleton3@76575e50

多线程安全单例模式(延迟/懒加载 使用静态内部类)

package demo28;

public class Singleton4 {
    private Singleton4() {
        System.out.println("初始化Singleton4..");
    }
    private static class Inner { //静态内部类
        static{
            System.out.println("Inner静态内部类加载了...");
        }
        private static Singleton4 s = new Singleton4();
    }
    private static Singleton4 getSingle() {
        System.out.println("获取Single实例...");
        return Inner.s;
    }


    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println(Singleton4.getSingle());
        new Thread(runnable,"t1").start();
        new Thread(runnable,"t2").start();
    }
}

运行结果

获取Single实例...
获取Single实例...
Inner静态内部类加载了...
初始化Singleton4..
demo28.Singleton4@1d8087e8
demo28.Singleton4@1d8087e8

从运行结果可以看出来,内部类和静态内部类都是延时加载的,也就是说只有在明确用到内部类时才加载。只使用外部类时不加载。

多线程安全单例模式(枚举实现)

package demo28;

/**
 * 目前最佳的单例写法——枚举模式—— 《Effective Java》
 */
public enum Singleton5 {

    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }

    public static void main(String[] args) {
        Singleton5.INSTANCE.doSomething();
    }
}

Effective Java这本书以后有机会拜读一下。这里就不细讲了,以后在慢慢研究一下枚举的用法。感兴趣的可以参考上面的链接。

并发容器

参考:https://blog.csdn.net/zl_StepByStep/article/details/88819859

多线程卖票问题

使用线程不安全的容器List

package demo29;

import java.util.ArrayList;
import java.util.List;

/**
 下面程序模拟卖票可能会出现两个问题:
    1.票卖重了
    2.还剩最后一张票时,好几个线程同时抢,出现-1张票
 出现上面两个问题主要是因为:
    1.remove()方法不是原子性的
    2.判断+操作不是原子性的
 */
public class TicketSeller1 {
    static List<String> tickets = new ArrayList<>();

    static {
        for (int i = 0; i < 10000; i++) {  //共一万张票
            tickets.add("票编号--" + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {   //共10个线程卖票
            new Thread(() -> {
                while (tickets.size() > 0) {  //判断余票
                    System.out.println("销售了..." + tickets.remove(0)); //操作减票
                }
            }).start();
        }
    }

}

运行结果

Exception in thread "Thread-5" java.lang.ArrayIndexOutOfBoundsException: -1
	at java.util.ArrayList.remove(ArrayList.java:501)
	at demo29.TicketSeller1.lambda$main$0(TicketSeller1.java:26)
	at java.lang.Thread.run(Thread.java:745)

使用线程安全的容器Vector

package demo29;

import java.util.Vector;

/**
 * 本程序虽然用了Vector作为容器,Vector中的方法都是原子性的,但是在判断size和减票的中间还是可能被打断的,即被减到-1张
 */
public class TicketSeller2 {
    static Vector<String> tickets = new Vector<>();  //Vector是一个同步容器

    static {
        for (int i = 0; i < 100; i++) tickets.add("票编号-" + i);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                while (tickets.size() > 0) {  //判断余票
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("销售了--" + tickets.remove(0));  //操作减票
                }
            }).start();
        }
    }

}

运行结果

Exception in thread "Thread-9" Exception in thread "Thread-8" Exception in thread "Thread-0" Exception in thread "Thread-1" Exception in thread "Thread-7" Exception in thread "Thread-5" Exception in thread "Thread-6" Exception in thread "Thread-4" Exception in thread "Thread-3" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
	at java.util.Vector.remove(Vector.java:831)
	at demo29.TicketSeller2.lambda$main$0(TicketSeller2.java:24)
	at java.lang.Thread.run(Thread.java:745)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
	at java.util.Vector.remove(Vector.java:831)
	at demo29.TicketSeller2.lambda$main$0(TicketSeller2.java:24)
	at java.lang.Thread.run(Thread.java:745)

在判断和操作放在同步代码块中

package demo29;

import java.util.LinkedList;
import java.util.List;

/*将判断和操作外面加锁,程序完全没有功能上的问题,但是效率很低*/
public class TicketSeller3 {
    static List<String> tickets = new LinkedList<>();

    static {
        for (int i = 0; i < 100; i++) {  //共100张票
            tickets.add("票编号:" + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {   //共10个线程卖票
            new Thread(() -> {
                while (true) {
                    synchronized (tickets) {
                        if (tickets.size() <= 0) break;  //判断 余票
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("销售了--" + tickets.remove(0)); //操作减票
                    }
                }

            }).start();
        }
    }
}

使用队列(Queue)来实现

package demo29;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * ConcurrentLinkedQueue底层不是加锁的实现,而是使用 CAS 原子指令来处理对数据的并发访问,非阻塞式,效率高很多
 */
public class TicketSeller4 {
    static Queue<String> tickets = new ConcurrentLinkedQueue<>();
    static {
        for (int i=0; i<1000; i++) {
            System.out.println("票编号:" + i );
        }
    }

    public static void main(String[] args) {
        for (int i=0; i<10; i++) {
            new Thread( ()-> {
                while(true) {
                    String str = tickets.poll(); //poll方法是原子性的,拿出一张票
                    //这里的判断和操作不是原子性的,但是也不会有线程安全问题,因为没有对queue做任何修改操作
                    if(str == null) break; //先poll,再判断tickets是不是空的,最后没有任何操作,所以不用加锁也不会出现任何问题
                    else System.out.println("销售了.." + str);
                }
            }).start();
        }
    }
}

List、Map

ConcurrentHashMap和ConcurrentSkipListMap

这里就简单讲一下使用,以后有机会在研究一下底层的实现。

package demo30;

import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

/**
 * 阅读ConcurrentSkipListMap
 * http://blog.csdn.net/sunxianghuang/article/details/52221913
 * 

* 比较一下ConcurrentHashMap和Hashtable的效率 *

* ConcurrentHashMap效率应该是比较高的,因为ConcurrentHashMap和Hashtable加锁的方式不一样。 * Hashtable是把自己整个都加锁了,而ConcurrentHashMap是采取锁分段。 */ public class T { public static void main(String[] args) { Map<String, String> map = new ConcurrentHashMap<>(); // Map map = new ConcurrentSkipListMap<>(); //高并发,排序。插入时效率比较低。查快 // Map map = new Hashtable<>(); // 所有操作加锁的,效率低 // Map map = new HashMap<>(); //没有锁,但是可以通过Collections.synchronizedXXXX去加锁 // Map map = new TreeMap<>(); //插入时要排序,所以插入可能会比较慢 Random r = new Random(); Thread[] threads = new Thread[100]; CountDownLatch latch = new CountDownLatch(threads.length); //门闩计数器 100 long start = System.currentTimeMillis(); //开始时间 for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 10000; j++) { //向map中加入1万个随机字符串 map.put("a" + r.nextInt(100000), "a" + r.nextInt(100000)); } latch.countDown(); //每执行一个线程,就countdown一次 }); } Arrays.asList(threads).forEach(Thread::start); //所有线程启动 try { latch.await(); //主线程在这等着,直到countdown到0 } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); //结束时间 System.out.println(end - start); //程序执行时间 } }

简单总结:Map和Set本质上是一样的,只是Set只有key,没有value,所以下面谈到的Map可以替换成Set。

  • 在不加锁的情况下,可以用:HashMap、TreeMap、LinkedHashMap。想加锁可以用Hashtable(用的非常少)。
  • 在并发量不是很高的情况下,可以用Collections.synchronizedXxx()方法,在该方法中传一个不加锁的容器(如Map),它返回一个加了锁的容器(容器中的所有方法加锁)!
  • 在并发性比较高的情况下,用ConcurrentHashMap ,如果并发性高且要排序的情况下,用ConcurrentSkipListMap。

CopyOnWriteArrayList 写时复制容器

这里就简单讲一下使用,以后有机会在研究一下底层的实现。

CopyOnWriteArrayList在多线程环境下,写时效率低,读时效率高,适合写少读多的环境,比如事件监听器。

package demo31;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 写时复制:添加元素的时候,会把这个容器复制一份,在复制的那份后面加一个新的,将引用指向复制的那份。
 * 读的时候不用加锁,适合写的很少,读的特别多的时候。 
 * */
public class T {
    public static void main(String[] args) {
        List<String> list =
//                new ArrayList<>();  //这个会出并发问题,最后size<100000,,运行时间:0.1秒多
//                new Vector<>(); //size=100000,,运行时间:0.1秒多
                new CopyOnWriteArrayList<>(); //size=100000,写效率很低,因为一直在"复制、写",运行时间:5秒多
        Random r = new Random();
        Thread[] threads = new Thread[100];
        for (int i=0; i<threads.length; i++) {  //起100个线程,每个线程向容器中加1000个数(最终应该是10万个数)
            Runnable task = () -> {
                for (int j=0; j<1000; j++) list.add("a" + r.nextInt());
            };
            threads[i] = new Thread(task);
        }
        runAndComputeTime(threads);
        System.out.println(list.size());
    }

    static void runAndComputeTime(Thread[] threads) {
        long start = System.currentTimeMillis();
        Arrays.asList(threads).forEach(Thread::start);
        Arrays.asList(threads).forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }
}

Collections.synchronizedList

返回一个加了锁的容器List

package demo32;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class T {

    public static void main(String[] args) {
        List<String> strs = new ArrayList<>();
        List<String> strsSync = Collections.synchronizedList(strs);
    }
}

Queue

ConcurrentLinkedQueue 并发队列

package demo33;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class ConcurrentQueue {
    public static void main(String[] args) {
        Queue<String> strs = new ConcurrentLinkedQueue<>();   //还有双端队列...Deque
        for (int i = 0; i < 10; i++) {
            //类似于add方法,如果是ArrayQueue,add方法可能会抛异常,但是offer方法不会抛异常,返回boolean类型即是否添加成功
            //strs.add("a" + i);
            strs.offer("a" + i);
        }
        System.out.println(strs);  //[a0, a1, a2, a3, a4, a5, a6, a7, a8, a9]
        System.out.println("队列原始大小:" + strs.size());   //队列原始大小:10
        //poll方法表示从头上拿出一个删掉;peek方法表示从头上拿出一个用一下不删。
        System.out.println("poll " + strs.poll() + "后的大小为:" + strs.size()); //poll a0后的大小为:9
        System.out.println("peek " + strs.peek() + "后的大小为:" + strs.size()); //peek a1后的大小为:9
    }
}

运行结果

[a0, a1, a2, a3, a4, a5, a6, a7, a8, a9]
队列原始大小:10
poll a0后的大小为:9
peek a1后的大小为:9

BlockingQueue 阻塞式队列

LinkedBlockingQueue 无界阻塞式队列
package demo33;

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/*无界阻塞式队列*/
public class LinkedBlockingQueueTest {
    static BlockingQueue<String> strs = new LinkedBlockingQueue<>();
    static Random r = new Random();

    public static void main(String[] args) {
        new Thread(() -> {  //1个生产者线程
            for (int i = 0; i < 100; i++) {
                try {
                    strs.put("a" + i);  //如果满了,就会等待
                    TimeUnit.MILLISECONDS.sleep(r.nextInt(100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "producer").start();

        for (int i = 0; i < 5; i++) {  //5个消费者进程
            new Thread(() -> {
                for (; ; ) {
                    try {
                        System.out.println(Thread.currentThread().getName()
                                + " take-" + strs.take()); //如果空了,就等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "customer" + i).start();
        }
    }
}
ArrayBlockingQueue 有界阻塞式队列
package demo33;

import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/*有界阻塞式队列*/
public class ArrayBlockingQueueTest {
    static BlockingQueue<String> strs = new ArrayBlockingQueue<>(10); //最多装10个
    static Random r = new Random();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            try {
                strs.put("a" + i);  //向容器中添加10个,就满了
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try { //strs已经满了,以下方法都加不进去,但是处理方式不同
            strs.put("aaa");//发现满了,就会等待,程序阻塞
//            strs.add("aaa");  //已经满了,再往里面装就会报异常
//            strs.offer("aaa");//不会报异常,但是加不进去,返回是否添加成功
//            strs.offer("aaa",1, TimeUnit.SECONDS); //1秒钟后加不进去,就不往里面加了
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(strs);
    }
}
DelayQueue 执行定时任务

往DelayQueue里加的元素是按时间排好序的,该队列是无界的。另外元素要实现Delayed接口,而Delayed接口又继承了Comparable接口,所以该类元素需要实现compareTo()方法;并且每个元素记载着自己还有多长时间才能被拿走,还要实现getDelay()方法。

package demo33;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayQueueTest {
    static DelayQueue<MyTask> tasks = new DelayQueue<>();

    static class MyTask implements Delayed {  //实现Delayed接口
        long runningTime;
        String name;

        MyTask(long rt, String name) {
            this.runningTime = rt;
            this.name = name;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS))
                return -1;
            else if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS))
                return 1;
            else  // ==
                return 0;
        }

        @Override
        public String toString() {
            return name + "--" + runningTime;
        }
    }

    public static void main(String[] args) {
        long now = System.currentTimeMillis();
        MyTask t1 = new MyTask(now + 1000, "task1"); //1 s 后执行 //②
        MyTask t2 = new MyTask(now + 2000, "task2"); //2 s后执行  //④
        MyTask t3 = new MyTask(now + 1500, "task3"); //1.5s后执行 //③
        MyTask t4 = new MyTask(now + 500, "task4");  //0.5s后执行 //①
        MyTask t5 = new MyTask(now + 2500, "task5"); //2.5s后执行 //⑤

        tasks.put(t1);
        tasks.put(t2);
        tasks.put(t3);
        tasks.put(t4);
        tasks.put(t5);

        System.out.println(tasks);
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println(tasks.take()); //按放进去的顺序拿出
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果

[task4--1578152889744, task1--1578152890244, task3--1578152890744, task2--1578152891244, task5--1578152891744]
task4--1578152889744
task1--1578152890244
task3--1578152890744
task2--1578152891244
task5--1578152891744
TransferQueue

适用场景:消费者先启动,生产者生产一个东西的时候,不扔在队列里,而是直接去找有没有消费者,有的话直接扔给消费者,若没有消费者线程,调用transfer()方法就会阻塞,调用add()、offer()、put()方法不会阻塞。TransferQueue适用于更高的并发情况

消费者先启动,然后调用transfer方法,这个时候不会阻塞

package demo33;

import java.util.concurrent.LinkedTransferQueue;

/**
 * 消费者先启动,生产者生产一个东西的时候,不扔在队列里,而是直接去找有没有消费者,有的话直接扔给消费者,
 * 若没有消费者线程,调用transfer()方法就会阻塞,调用add()、offer()、put()方法不会阻塞。
 */
public class TransferQueueTest {
    public static void main(String[] args) throws InterruptedException {
        LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();
        new Thread(() -> { //消费者先启动,可以拿走aaa
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        strs.transfer("aaa");
    }
}

运行结果

aaa

如果是先调用transfer方法,然后在启动消费者线程,这个时候就会阻塞了。

package demo33;

import java.util.concurrent.LinkedTransferQueue;

/**
 * 消费者先启动,生产者生产一个东西的时候,不扔在队列里,而是直接去找有没有消费者,有的话直接扔给消费者,
 * 若没有消费者线程,调用transfer()方法就会阻塞,调用add()、offer()、put()方法不会阻塞。
 */
public class TransferQueueTest2 {
    public static void main(String[] args) throws InterruptedException {
        LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();

        strs.transfer("aaa");
        new Thread(() -> { //消费者在生产者后启动,拿不到aaa,程序阻塞
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

    }
}

如果是调用put方法的话就不会阻塞

package demo33;

import java.util.concurrent.LinkedTransferQueue;

/**
 * 消费者先启动,生产者生产一个东西的时候,不扔在队列里,而是直接去找有没有消费者,有的话直接扔给消费者,
 * 若没有消费者线程,调用transfer()方法就会阻塞,调用add()、offer()、put()方法不会阻塞。
 */
public class TransferQueueTest3 {
    public static void main(String[] args) throws InterruptedException {
        LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();

        strs.put("aaa"); //如果用put的话就不会阻塞了
        new Thread(() -> { //后启动消费者
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

    }
}

运行结果

aaa
SynchronizedQueue

特殊的TransferQueue,容量为0。扔在队列的东西必须被消费者马上消费掉,否则就会出问题。

package demo33;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;

/*一种特殊的TransferQueue,生产的任何一个东西必须直接交给消费者消费,不能搁在容器里,容器的容量为0*/
public class SynchronizeQueueTest {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> strs = new SynchronousQueue<>();

        new Thread(() -> {  //消费者线程
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        strs.put("aaaa"); //不能调用add(报错),add不进去,put阻塞,等待消费者消费,内部调用的transfer.
        System.out.println(strs.size());  //0
    }
}

运行结果

0
aaaa

参考

https://www.bilibili.com/video/av33688545?p=20

设计模式之单例模式(线程安全)

Java单例模式:为什么我强烈推荐你用枚举来实现单例模式

https://blog.csdn.net/zl_StepByStep/article/details/88819859

源代码

https://gitee.com/cckevincyh/java_concurrent_learning

你可能感兴趣的:(Java)