多线程编程总结二(包含并发容器和线程池)

关于多线程基础部分内容可以看多线程编程总结

1.看看下面的代码

package com.yan.thread;

import java.util.concurrent.TimeUnit;

public class Account {
    private  String name;
    private double balance;

    public synchronized void set(String name,double balance) {
        this.name = name;
        try{
            Thread.sleep(2000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        this.balance = balance;
    }

    public double get(String name){
        return this.balance;
    }

    public static void main(String[] args) {
        Account a = new Account();
        new Thread(()->a.set("zhangsan",100)).start();
        try{
            TimeUnit.SECONDS.sleep(1);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(a.get("zhangsha"));
        try{
            TimeUnit.SECONDS.sleep(2);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(a.get("zhangsna"));
    }
}

当写操作加了锁,读操作不加锁时,会发现会有脏读问题。因为写操作的线程在执行过程中sleep还没执行完,读操作的线程可能已经去获取对应值。默认初始化为0。所以两次输出,一次为0.0,一次为100.0。给读方法加上锁就能解决这个问题。

 

2.同步遇到异常锁是会被释放的,可能出现数据不一致

public class T {
	int count = 0;
	synchronized void m() {
		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) {
				int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
			}
		}
	}
	
	public static void main(String[] args) {
		T t = new T();
		Runnable r = new Runnable() {

			@Override
			public void run() {
				t.m();
			}
			
		};
		new Thread(r, "t1").start();
		
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		new Thread(r, "t2").start();
	}
	
}

3.volatile关键字

public class T {
	/*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
	void m() {
		System.out.println("m start");
		while(running) {
			/*
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}*/
		}
		System.out.println("m end!");
	}
	
	public static void main(String[] args) {
		T t = new T();
		
		new Thread(t::m, "t1").start();
		
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		t.running = false;
		
		
	}
	
}

上面的程序在我们希望的情况下应该是程序可以执行完,输出m end! 但实际并不是。这是由于线程在工作时会去主内存中去拷贝running这个变量,放在自己的工作内存中。也就是说每个线程都持有running变量的副本。修改也是修改的副本,另一个修改running变量值为false后,会再写回主内存,但是陷入循环的那个线程却不定会去刷新这个值,所以就依旧为true。程序不会执行完。这个时候可以用volatile修饰共享变量。volatile关键字保证内存可见性,当某线程把值更新到主内存时,其他线程可以及时看到。原因可以看加了volatile后生成的汇编代码,会发现多了个锁前缀指令。这个指令会让被修饰的变量被修改后强制写入主内存。其他工作内存对该变量的引用失效,需要重新从主内存中取。

总结:volatile保证内存可见性,除此之外还禁止指令的重排序。但是volatile不能保证原子性,修改共享会有数据一致性问题。理想情况是会输出100000;但多运行几次可能会出现小于100000的情况。

public class T {
	volatile int count = 0; 
	void m() {
		for(int i=0; i<10000; i++) count++;
	}
	
	public static void main(String[] args) {
		T t = new T();
		
		List<Thread> threads = new ArrayList<Thread>();
		
		for(int i=0; i<10; i++) {
			threads.add(new Thread(t::m, "thread-"+i));
		}
		
		threads.forEach((o)->o.start());
		
		threads.forEach((o)->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		
		System.out.println(t.count);
		
		
	}
	
}

这是由于虽然我们保证了工作内存的最新,但是非原子性操作,比如i++;可能存在A线程读取了i的最新值后切换到B线程执行,B线程执行加1操作后,由于volatile特性值会及时更新到主内存,A的工作内存也失效,但是A的读取操作已经完成不会再去取,切换到A线程会拿原本的i的值进行加1操作。

适合用volatile的场景:

1.运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

2.变量不需要与其他的状态变量共同参与不变约束。

 

4.为了解决上述问题,可以加synchronized,但是这个比较重,影响效率,可以使用java给我们提供的原子类进行操作。

public class T {
	/*volatile*/ //int count = 0;
	
	AtomicInteger count = new AtomicInteger(0); 

	/*synchronized*/ void m() { 
		for (int i = 0; i < 10000; i++)
			//if count.get() < 1000
			count.incrementAndGet(); //count++
	}

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

		List<Thread> threads = new ArrayList<Thread>();

		for (int i = 0; i < 10; i++) {
			threads.add(new Thread(t::m, "thread-" + i));
		}

		threads.forEach((o) -> o.start());

		threads.forEach((o) -> {
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		System.out.println(t.count);

	}

}

这个程序就能按理想结果输出100000;count.incrementAndGet();是count++的替代,不同的是个原子操作。需要注意的是多个原子类操作不是原子操作。就比如连续两个count.incrementAndGet();每个是原子类操作,但是这两个语句中间可能存在线程切换。

5.说明锁是在堆内存中的

public class T {
	
	Object o = new Object();

	void m() {
		synchronized(o) {
			while(true) {
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName());
				
				
			}
		}
	}
	
	public static void main(String[] args) {
		T t = new T();
		//启动第一个线程
		new Thread(t::m, "t1").start();
		
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//创建第二个线程
		Thread t2 = new Thread(t::m, "t2");
		
		t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
		
		t2.start();
		
	}

	

}

6.不要以字符串常量作为锁定对象,因为你的程序和你用到的类库不经意间使用了同一把锁,可能发生的死锁阻塞。

7.ReentrantLock

使用syn锁定的话如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此经常在finally中进行锁的释放。

使用ReentrantLock的tryLock,这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待,返回Boolean类型,不等待可以执行其他操作。

ReentrantLock调用lockInterruptibly方法,可以对线程interrupt方法做出响应。举个例子,线程一长时间获得锁不释放,线程二lock()方法会一直等待获取锁,用lockInterruptibly()说明这个过程是可打断的,主线程或其他线程可以用interrupt()打断这个等待锁状态。当然没获取到锁,原本线程二的内容也就不执行了。

ReentrantLock可以指定为公平锁。

private static ReentrantLock lock=new ReentrantLock(true); 

像synchronized是非公平锁,就是一堆线程抢夺资源,等待久的并不一定先获取,不公平,不过效率也高。公平锁根据等待时间,两条线程的情况下,可以看哪条线程运行结果相对均匀

8.ThreadLocal

用于每个线程特有的变量。各个线程之间的值互不影响,每个线程维护着一个Map

9.常见并发容器

平时用的HashMap、ArrayList都不是线程安全的。高版本的JDK提供了升级版的对应线程安全的类,比如ConcurrentHashMap,对应HashMap,不是全局加锁,而是把内容分段加锁,这样各段之间的并发访问,自己加自己的锁,互不影响,添加元素和获取元素都要经过两次定位,先确定哪个段(segment),然后确定在段中的位置。除此之外还有ConcurrentSkipListMap,支持排序。

CopyOnWriteArrayList支持读多写少的情况,原理是每次修改复制一份原有的数据,改完再把引用指向新的地址,所以不影响读。也就是说增删改加锁,读操作不用加锁。

当然还有通用的Collections.synchronizedXXX工具类加锁,Collections.synchronizedList(list)传进非线程安全的list,加锁获取线程安全的list,类似还有SynchronizedMap。

Queue(基本方法包括add(E),添加元素;offer(E),添加元素,和add区别是queue为空offer不抛异常返回null,add抛异常(下面的一样);
element() 获取队头元素,queue为空抛异常;peek(),获取队头元素;
poll(),获取队头元素,并移除这个元素;
remove(),获取队头元素,并移除这个元素,和poll区别是queue为空remove抛异常;)体系下的

ConcurrentLinkedQueue

BlockingQueue:阻塞的队列,新增了一些阻塞相关的方法,比较重要的有put()和take(),跟原本的存取方法区别在于这两个就是阻塞的,具体就是如果队列满了,put阻塞。队列空了,take阻塞;也就是说上一篇多线程编程总结写的比较复杂的生产者消费者的例子,用了wait和notifyAll或者await和signal去协调线程写的容易吐血的例子,用上BlockingQueue这些协调都能省去。举个例子

public class _LinkedBlockingQueue {

	static BlockingQueue<String> strs = new LinkedBlockingQueue<>();

	static Random r = new Random();

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

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

		}
	}
}

 

BlockingQueue子类

ArrayBlockingQueue

LinkedBlockingQueue

DelayQueue:延时获取任务的无界队列,每个任务记录着还有多少时间可以被消费者取出。列头的元素是最先“到期”的元素(内部维护着优先级队列PriorityQueue来保证),没有到期的元素会一直阻塞。put的参数任务必须实现delayed接口,重写getDelay()才知道每个元素还剩多少时间往外取。这个接口是继承comparable接口,所以要重写compareto()。可以用于执行定时任务

public class _DelayQueue {

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

	static Random r = new Random();
	
	static class MyTask implements Delayed {
		long runningTime;
		
		MyTask(long rt) {
			this.runningTime = rt;
		}

		@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 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());
		}
	}
}

TransferQueue

LinkedTransferQueue采用一种预占模式。就是消费者线程取元素时,如果队列不空,就直接取,若队列是空的,那就生成一个节点(节点元素为null)入队,然后消费者线程就在旁边等着,生产者发现有null节点就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。

核心方法:transfer(E):将元素给消费者,如果没有消费者就会入队,阻塞等待(示例程序要消费者线程先启动)。下面是TransferQueue一些方法

public interface TransferQueue<E> extends BlockingQueue<E> {
    // 是否有消费者等待接收,有就给他,否则返回false,并且不进入队列。
    boolean tryTransfer(E e);
    // 是否有消费者等待接收,有就给他,否则阻塞知道有消费者接收。
    void transfer(E e) throws InterruptedException;
    // 和上面的相比多了超时时间
    boolean tryTransfer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
    // 是否有消费者在等待
    boolean hasWaitingConsumer();
    // 获取所有等待获取元素的线程数目
    int getWaitingConsumerCount();
}
SynchronousQueue
可以看做长度为0的TransferQueue。容器空的,直接给消费者处理。put方法就相当于TransferQueue的transfer方法。

10.常见的几种线程池

关于线程池可以看这篇---深入理解线程池

Executors.newFixedThreadPool:长度固定的线程池。适用执行长期的任务,使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。

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

Executors.newCachedThreadPool:没有核心线程,所以任务直接加到SynchronousQueue队列,需要几个起几个线程,当然受资源限制不可能无上限,用于并发执行大量短期的小任务。

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

Executors.newSingleThreadExecutor:单个线程的线程池,可用于顺序执行一些任务。

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

Executors.newScheduledThreadPool:可用于执行定时任务的线程池。有没有想到上面说的DelayQueue,不过他并不是通过DelayQueue实现的

 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

附加个demo

public class T10_ScheduledPool {
	public static void main(String[] args) {
		ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
		service.scheduleAtFixedRate(()->{
			try {
				TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName());
		}, 0, 500, TimeUnit.MILLISECONDS);
		
	}
}
Executors.newWorkStealingPool:工作窃取线程池,饥饿状态的线程会主动去取任务执行。里面其实封装着ForkJoinPool。
   public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }
ForkJoinPool:充分利用CPU优势,支持将一个任务拆分成多个“小任务”并行计算,拆分后要是还是太大继续拆分,到自定义满足的要求,再把多个“小任务”的结果合并成总的计算结果。fork就是拆分过程,join合并过程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值