2019.7.11笔记——java线程并发相关

本文深入解析Java中的多种并发容器,包括ThreadLocal、Vector、ConcurrentLinkedQueue、LinkedBlockingQueue等,对比它们的性能与适用场景,帮助开发者选择合适的并发工具。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

守护进程

main即是线程也是进程
设置了守护进程的线程在其他没有设置守护进程的线程报错的情况下也会中断,没有设置则会继续执行
当然设置要在线程执行之前设置
/**
 * T2线程能否执行?
 */
public class Demo {

    int count = 0;

    synchronized void test(){
        System.out.println(Thread.currentThread().getName() + " start......");
        while (true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                //碰到异常的情况,如果没有处理,会自动释放锁,所以T2可以执行。
                int i = 1/0;
            }
        }
    }

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

        Runnable r = () -> demo11.test();

        new Thread(r, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread t2 = new Thread(r, "t2");
        t2.setDaemon(true);
        t2.start();
    }

}

在这里插入图片描述


ThreadLocal

ThreadLocal是线程的本地变量,每个线程一一对应,这个对象和线程绑定了,线程封闭,也就是说变量是线程私有的,其他的线程是访问不到的。

ThreadLocal<泛型> threadLocal = new ThreadLocal<泛型>();
通过set方法设置值和get方法取值
在下面这个程序中第二个线程是拿不到第一个线程设置的值的,拿到的是null,而第一个是拿得到自己设置的对象
public class TLDemo2 {

    private static ThreadLocal<User> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            threadLocal.set(new User());
            System.out.println(threadLocal.get());
        }).start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            }
//            threadLocal.set(new User());
//            threadLocal.remove();
            System.out.println(threadLocal.get());
        }).start();

    }

}

Vector

vector是一个同步容器,有序的集合
Vector<泛型> vector = new Vector<泛型>()
add方法存入值,remove方法去除值

这里是判断和操作分离了,虽然说在vector里面remove方法是原子性的,但是他的判断和remove方法是分离的,但是可能在判断到remove的过程当中线程可能会被打断。我们可以加一个模拟性的睡眠,因为在你实际开发的时候,可能在这中间有些判断代码逻辑代码。如果剩了最后一个了,很多线程去抢票,虽然size是原子性的,remove是原子性的,但是在他们的中间,线程还是有可能被打断。

/**
 * 有10000张火车票,同时有10个窗口对外售票
 * 请写一个模拟程序
 */
public class SaleOfTickets2 {

	private static Vector<Integer> tickets = new Vector<>();
	
	static {
		for (int i = 0; i < 10000; i++) {
			tickets.add(i);
		}
	}
	/**
	 * vector是一个同步容器
	 * 这里是判断和操作分离了,虽然说在vector里面remove方法是原子性的,但是他的判断和remove方法是分离的,
	 * 但是可能在判断到remove的过程当中线程可能会被打断。我们可以加一个模拟性的睡眠,因为在你实际开发的
	 * 时候,可能在这中间有些判断代码逻辑代码。
	 * 如果剩了最后一个了,很多线程去抢票,虽然size是原子性的,remove是原子性的,但是在他们的中间,
	 * 线程还是有可能被打断
	 */
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				while(tickets.size() > 0){
					try {
						TimeUnit.MILLISECONDS.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("销售票编号:" + tickets.remove(0));
				}
			}).start();
		}
	}
}

为了解决这个问题可以将这两个原子操作和中间的代码一起用一个synchronized框起来,只要加了锁就可以保证了整体的原子性。

public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				while(true){
					//这里使用synchronized,使两个操作具备了原子性,不会出问题
					synchronized(tickets){
						if(tickets.size() <= 0){
							break;
						}
						System.out.println("销售票编号:" + tickets.remove(0));
					}
				}
			}).start();
		}
	}

Queue

Queue总的来说是个队列的接口,在jdk1.5之后就有许多实现了这个接口的并发容器。

ConcurrentLinkedQueue

这是个并发单向链表队列,线程安全的,无界的,可设置边界,add方法加值和poll取值

这里用了一个concurrentlinkedqueue,并发的链表队列。线程里面调用了一个poll方法,意思是往外面拿一个数据,相当于在尾巴里面拿一个,如果没有拿到,他的返回值就是空,那么就中断线程。

这里面没有加锁,同样有判断,但是不会出问题。完成卖票功能这种效率是比较高的。queue里面是不能装空值。

这里虽然判断和操作是一起的,但是我们没有在判断里面有任何操作,大不了反过头来再拿一边,poll底层实现是cas,这里我们就不用加锁了。

/**
 * 有10000张火车票,同时有10个窗口对外售票
 * 请写一个模拟程序
 *
 * 在JDK1.5以后,java里面提供了很多的并发容器,这里我们用的是一个queue,队列。
 * 所谓队列其实就是一个容器,就是站成一对,不管票还是人都在里面排成一堆,队列有几种,有先进先出的,
 * 还有两端的队列,还有就是栈,先进后出,先加进去的后出来。
 * 这里用了一个concurrentlinkedqueue,并发的链表队列。线程里面调用了一个poll方法,
 * 意思是往外面拿一个数据,相当于在尾巴里面拿一个,如果没有拿到,他的返回值就是空,那么就中断线程。
 * 这里面没有加锁,同样有判断,但是不会出问题。完成卖票功能这种效率是比较高的。queue里面是不能装空值。
 * 这里虽然判断和操作是一起的,但是我们没有在判断里面有任何操作,大不了反过头来再拿一边,
 * poll底层实现是cas,这里我们就不用加锁了。
 */
public class SaleOfTickets4 {

	private static Queue<Integer> tickets = new ConcurrentLinkedQueue<>();
	
	static {
		for (int i = 0; i < 10000; i++) {
			tickets.add(i);
		}
	}
	
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				while(true){
					Integer poll = tickets.poll();
					if(poll == null){
						break;
					}
					System.out.println("销售票编号:" + poll);
				}
			}).start();
		}
	}
}
ConcurrentLinkedQueue还有许多方法存储值和取值
offer取值,相当于add因为add就是用offer实现的,peek取值但不移除值
public class Demo {

	public static void main(String[] args) {
		Queue<String> strings = new ConcurrentLinkedQueue<>();
		
		for (int i = 0; i < 10; i++) {
			//offer,类似于add方法,add会出一些问题,比如容量限制,
			//超出限制会抛异常,offer有返回值可以判断是否加成功了
			strings.offer("a" + i);
		}
		
		System.out.println(strings);
		
		System.out.println(strings.size());

		System.out.println(strings.poll());//拿了就没了
		System.out.println(strings.size());

		System.out.println(strings.peek());//用一下不删
		System.out.println(strings.size());
	}
}

LinkedBlockingQueue

阻塞式的队列 ,无界的 ,可设置边界 ,put方法存储值,take方法取值
put和take方法会自己阻塞,没有值和值过多的时候会自己等待
/**
 * LinkedBlockingQueue
 * 阻塞式的容器
 *
 */
public class Demo {

	private static BlockingQueue<String> strings = new LinkedBlockingQueue<>(10);

	public static void main(String[] args) {
		new Thread(()->{
			for (int i = 0; i < 100; i++) {
				try {
					// 在阻塞式容器里面加了一个方法,put,也就是如果满了就会等待,对应的方法叫take,如果空了就会等待。
					// 这种容器我们去用的时候自动就实现了阻塞式的生产者消费者。
					strings.put("商品" + i);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, "producer").start();

		for (int i = 0; i < 5; i++) {
			new Thread(()->{
				for(;;){
					try {
						// take,拿,如果空了也会阻塞
						System.out.println(Thread.currentThread().getName() + " take " + strings.take()); //如果空了,就会等待
					} catch (Exception e) {
						e.printStackTrace();
					} 
				}
			},"consumer" + i).start();
		}

	}

}

ArrayBlockingQueue

阻塞式的队列,有界的,必须设置边界,否则报错,其他和LinkedBlockingQueue一致

需要注意的是offer虽然不会阻塞但也不会报错,但是如果队列满了,值是加不进去的,可以通过offer的返回值判断,true就是加进去了,false是没有加进去。

offer可以设置时间,如果这段时间加不进去就不加了也就是返回false。

/**
 * ArrayBlockingQueue
 *
 * 有界队列,意思就是说这个队列能装的元素的个数是固定的,后面讲线程池的时候,里面装的其实是一个个任务。
 * 这里只能装10个,如果超过了可能会出问题可能会阻塞,这里看你调用什么方法。
 * add会报异常
 * offer不会报异常,他只通过布尔类型的返回值来告诉你是加成功了还是没有加成功。
 * offer可以设置时间,如果这段时间加不进去就不加了也就是返回false
 * put方法是满了会阻塞住。
 */
public class Demo {

	private static BlockingQueue<String> strings = new ArrayBlockingQueue<>(10);
	
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 10; i++) {
			strings.put("a" + i);
		}
		strings.add("aaaa");
//		strings.put("aaaa");
//		strings.offer("aaaa");
		strings.offer("aaaa",1, TimeUnit.SECONDS);
		System.out.println(strings);
	}
}
add方法会报错
put方法会阻塞
offer方法会继续执行但是值没加进去
offer带时间参数的方法会等待然后继续执行依然没加进去

DelayQueue

DelayQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素
如果延迟都还没有期满,则队列没有头部,并且 使用poll方法 将返回 null

而且如果要存储的对象必须实现Delayed接口,下面MyTask类就实现了Delayed接口,并且在创建该类的实例时还需要设置延迟的时间。

/**
 * DelayQueue
 * 容器里每一个元素都设置了一个时间,时间到了才能从中提取元素
 */
public class Demo {

	private static BlockingQueue<MyTask> tasks = new DelayQueue<MyTask>();

	static class MyTask implements Delayed{

		long runningTime;
		
		public MyTask(long rt) {
			this.runningTime = rt;
		}

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

		@Override
		public long getDelay(TimeUnit unit) {
			return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
		}
		
		@Override
		public String toString() {
			return "" + runningTime;
		}
		
		public static void main(String[] args) throws InterruptedException {
			long now = System.currentTimeMillis();
			MyTask t1 = new MyTask(now+1000);
			MyTask t2 = new MyTask(now+2000);
			MyTask t3 = new MyTask(now+1500);
			MyTask t4 = new MyTask(now+2500);
			MyTask t5 = new MyTask(now+500);
			
			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++) {
				System.out.println(tasks.take());
			}
		}

	}

}

LinkedTransferQueue

和普通的queue的方法差不多,多了一个transfer方法
transfer方法可以直接把值交给消费者不会放在消息队列里,所以效率会很高,如果没有消费者就会阻塞在哪
/**
 * TransferQueue
 * 和普通的queue的方法差不多,多了一个transfer方法。
 * 如果你用这种队列的话,往往是消费者先启动,生产者生产一个东西的时候,他先是去找消费者,
 * 如果有消费者就直接丢给消费者。
 */
public class Demo {

	public static void main(String[] args) throws InterruptedException {
		LinkedTransferQueue<String> strings = new LinkedTransferQueue<>();
		
		new Thread(()->{
			try {
				System.out.println("t1"+strings.take());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}).start();

		new Thread(()->{
			try {
				System.out.println("t2"+strings.take());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}).start();

		TimeUnit.SECONDS.sleep(2);

		strings.transfer("aaa");
//		strings.put("aaa");
		System.out.println(strings.size());
//		new Thread(()->{
//			try {
//				System.out.println(strings.take());
//			} catch (Exception e) {
//				e.printStackTrace();
//			}
//		}).start();
	}

}

SynchronousQueue

 同步队列,容量为0  
 不能使用add方法存数据,因为add会把数据放入队列,但是队列容量为0,所以会报错,只能用put方法
/**
 * SynchronizedQueue 同步队列
 * 同步队列是容量为0,也就是来的东西必须给消费掉.
 * 首先启动一个消费者,调用add方法,他报错了
 * 只能调用put,意思就是阻塞等待消费者消费。put里面其实用的是transfer,任何东西必须消费,不能往容器里面扔。
 */
public class Demo {

	public static void main(String[] args) throws InterruptedException {
		BlockingQueue<String> strings = new SynchronousQueue<>();
		
		new Thread(()->{
			try {
				System.out.println(strings.take());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}).start();
//		strings.add("aaa");
		strings.put("aaa");
		strings.put("aaa");
		strings.put("aaa");
		strings.put("aaa");
		strings.put("aaa");
		System.out.println(strings.size());
	}
}

CopyOnWriteArrayList

写的时候复制写,操作很复杂,很耗时,但读的效率很高,而且线程安全
所以在如果使用的集合很多时候都是读而很少写入的情况下建议使用这个集合

在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。

这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁。

下面这端代码可以测试ArrayList、Vector和CopyOnWriteArrayList三种集合性能,可以看出ArrayList是线程不安全的,Vector和CopyOnWriteArrayList可以很好地保障线程的安全。但是CopyOnWriteArrayList也是耗时最长的。

/**
 * CopyOnWriteList 写时复制容器
 *
 * 在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,
 * 然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。
 * 这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,
 * 这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁。
 *
 * 读多写少可以用copyonwritelist
 *
 * 下面例子看看效率
 */
public class Demo {

	public static void main(String[] args) {
//		List<String> lists = new ArrayList<>();
//		List<String> lists = new Vector<>();
		List<String> lists = new CopyOnWriteArrayList<>();
		Random r = new Random();
		Thread[] threads = new Thread[100];
		
		for (int i = 0; i < threads.length; i++) {
			Runnable task = new Runnable() {
				@Override
				public void run() {
					for (int j = 0; j < 1000; j++) {
						lists.add("A" + r.nextInt(10000));
					}
				}
			};
			threads[i] = new Thread(task);
		}
		
		run(threads);
		
		System.out.println(lists.size());
	}

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

注意ArrayList线程不安全


Collections

这是一个集合工具类
Collections.synchronizedList(arrayLsit)——可以使得arraylist变成一个线程安全的集合
	public static void main(String[] args) {
		ArrayList<String> arrayList = new ArrayList<>();
		List<String> synchronizedList = Collections.synchronizedList(arrayList);
	}

它可以把很多不同步容器转换成一个线程安全的容器
在这里插入图片描述


Map

HashTable

hashtable所有方法都加了锁,hashtable往里面加任何数据的时候都是要锁定整个对象

ConcurrentHashMap

concurrenthashmap,是分成十六个段,每次插数据的时候,只会锁住一小段,1.8之后实现不同
是一种线程安全的HashMap  
比hashtable效率高

ConcurrentSkipListMap

线程安全有序的hashmap

性能比较

/**
 * ConcurrentHashMap
 * 并发的hashmap,这个例子测试一下效率
 *
 * 第一种用hashtable,hashtable所有方法都加了锁了,第二种concurrenthashmap,
 * 大致能看出来他的效率要比hashtable要高一些,在多线程的情况下。
 * 为什么呢,因为hashtable往里面加任何数据的时候都是要锁定整个对象,
 * 而concurrenthashmap,是分成十六个段,每次插数据的时候,只会锁住一小段,1.8之后实现不同。
 */
public class Demo {

	public static void main(String[] args) {
//		Map<String, String> map = new ConcurrentHashMap<>();
		Map<String, String> map = new ConcurrentSkipListMap<>();
//		Map<String, String> map = new Hashtable<>();

//		Map<String, String> map = new HashMap<>();
//		Map<String, String> map1 = Collections.synchronizedMap(map);

		Random random = new Random();
		Thread[] threads = new Thread[100];
		CountDownLatch latch = new CountDownLatch(threads.length);
		long start_time = System.currentTimeMillis();
		for (int i = 0; i < threads.length; i++) {
			threads[i] = new Thread(()->{
				for(int j=0; j<10000;j++) {
					map.put("a" + random.nextInt(100000), "a" + random.nextInt(100000));
//					map1.put("a" + random.nextInt(100000), "a" + random.nextInt(100000));
				}
				latch.countDown();
			});
		}
		Arrays.asList(threads).forEach(t->t.start());
		try {
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		long end_time = System.currentTimeMillis();
		System.out.println(end_time-start_time);
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值