线程问题
线程同步
1.synchronized
2.wait、notify
3.线程安全与非安全
StringBuffer 、StringBuilder
Vector、Hashtable
ArrayList、HashMap
Collections.synchonizedList()
Collections.synchronizedMap()
4.ExecutorService
5.BlockingQueue
----------------------------------------------------------------------------
1. synchronized同步锁
synchronized
1.可以修饰方法 被修饰的方法 称为同步方法
2.synchronized修饰代码块,用于同步某一块代码码片段的, 通常synchronized块的范围要小于synchronized方法
多个线程并发读写同一个临界资源时候会发生"线程并发安全问题“
常见的临界资源:
多线程共享实例变量
多线程共享静态公共变量
若想解决线程安全问题,需要将异步的操作变为同步操作。
何为同步?那么我们来对比看一下什么是同步什么异步。
所谓异步操作是指多线程并发的操作,相当于各干各的。
所谓同步操作是指有先后顺序的操作,相当于你干完我再干。
而java中有一个关键字名为:synchronized,该关键字是同步锁,用于将某段代码变为同步操作,从而解决线程并发安全问题。
1 /** 2 * 线程安全问题 3 *多线程并发访问同一段数据的时候 就会产生线程安全问题 4 *解决办法: 把异步操作变成同步操作(先后顺序) 5 *synchronized 同步锁 也有人叫 互斥锁 6 */ 7 class SynDemo{ 8 public static void main(String[] args) { 9 final Table table =new Table(); 10 Thread t1 =new Thread(){ 11 @Override 12 public void run() { 13 while(true){ 14 System.out.println(getName()+":"+table.getBean()); 15 Thread.yield(); 16 } 17 } 18 }; 19 20 Thread t2=new Thread(){ 21 @Override 22 public void run() { 23 while(true){ 24 System.out.println(getName()+":"+table.getBean()); 25 Thread.yield(); 26 } 27 } 28 }; 29 t1.start(); 30 t2.start(); 31 32 } 33 } 34 class Table{ 35 //桌子上有20个桌子 36 private int beans =20; 37 //从桌子上取出一个豆子 38 public synchronized int getBean(){ 39 if(beans==0){ 40 throw new RuntimeException("没有豆子了"); 41 } 42 Thread.yield();//主动让cpu回到Runnable 43 return beans--; 44 } 45 } 46 /** 47 * 线程安全的互斥问题 48 * @author Administrator 49 *当一个类中多个方法被synchronize修饰时 这些方法一般是互斥的 50 */
2. 锁机制
Java提供了一种内置的锁机制来支持原子性:
同步代码块(synchronized 关键字 ),使用同步块的目的: 在于缩小同步范围来提高并发效率
同步代码块包含两部分:
a、作为锁的对象的引用,
b、作为由这个锁保护的代码块。
synchronized (同步监视器—锁对象引用this){ //代码块 }
通常 同步监视器-锁对象引用 写的是this
若方法所有代码都需要同步也可以给方法直接加锁。
每个Java对象都可以用做一个实现同步的锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时怎释放锁,而且无论是通过正常路径退出锁还是通过抛异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
3. 选择合适的锁对象
使用synchroinzed需要对一个锁对象上锁以保证线程同步。
那么这个锁对象应当注意:多个需要同步的线程在访问该同步块时,看到的应该是同一个所对象引用。否则达不到同步效果。通常我们会使用this来作为锁对象。
同步块要想有同步效果,多线程看到的同步锁对象,必须是同一个
4. 选择合适的锁范围
在使用同步块时,应当尽量在允许的情况下减少同步范围,以提高并发的执行效率。
5. 静态方法锁
当我们对一个静态方法加锁,如:
public synchronized static void xxx(){ …. }
那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class。
静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的。
原因在于,静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。
静态方法上锁以后 同步是跨对象的
** * 静态方法锁 * 静态方法上锁后,同步是跨对象的 * @author Administrator * */ public class StaticDemo { public static void main(String[] args) { } public void methodA(){ String name =Thread.currentThread().getName(); System.out.println(name+"调用了methodA方法"); try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("调用MethodA方法完毕"); } } public synchronized static void methodB(){ String name =Thread.currentThread().getName(); System.out.println(name+"调用了methodB静态方法"); try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("调用MethodB方法完毕"); } } } class TestDemo{ public static void main(String[] args) { final StaticDemo sd1 =new StaticDemo(); final StaticDemo sd2 =new StaticDemo(); Thread t1 =new Thread(){ @Override public void run() { sd1.methodB(); } }; Thread t2 =new Thread(){ @Override public void run() { sd2.methodB(); } }; t1.start(); t2.start(); } }
/** * 编写计时线程,每隔5秒钟输出当前的日期-时间, * 主线程结束后计时完毕 * @author Administrator * */ class Homework1{ /* * 1.创建一个线程 用于计时 * 2.线程计时 * 2.1 创建SimpleDateFormate * 2.2循环一下操作 * 2.3创建Date实例 表示系统时间 * 2.4使用SimpleDateFormate将Date转换为字符串输出 * 2.5阻塞线程5000毫秒 * 3.设置线程为守护线程 * 4.线程启动 * 5.为了保证守护线程可以运行一段时间 我们阻塞main线程10秒钟 */ public static void main(String[] args) { Thread t1 =new Thread(){ @Override public void run() { while(true){ SimpleDateFormat sdf =new SimpleDateFormat("yy-MM-dd HH:mm:ss"); Date now =new Date(); System.out.println(sdf.format(now)); try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }; t1.setDaemon(true); t1.start(); try { //如果不阻塞main 只剩下守护进程的时候 gc直接调出 结束进程了 Thread.sleep(10000000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
6、wait和notify
多线程之间需要协调工作。
例如,浏览器的一个显示图片的 displayThread想要执行显示图片的任务,必须等待下载线程downloadThread将该图片下载完毕。如果图片还没有下载完,displayThread可以暂停,当downloadThread完成了任务后,再通知displayThread“图片准备完毕,可以显示了”,这时,displayThread继续执行。
以上逻辑简单的说就是:如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒。
在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。
wait (阻塞) 可以在当前对象身上等待
notify (解除阻塞) 调用哪个对象的notify方法 就可以让在该对象身上等待的线程继续运行
join比较被动 需要都运行完 才会 解除阻塞
wait方法要求: 调用哪个对象的wait的方法 就要将该对象加锁
1 /** 2 * wait 和notify方法 3 * 这两个方法是定义在Object上的 4 */ 5 class WaitAndNotify{ 6 private static boolean isFinish =false; 7 public static void main(String[] args) { 8 //用一个对象测试wait和notify 9 final Object obj =new Object(); 10 //下载线程 11 final Thread download =new Thread(){ 12 @Override 13 public void run() { 14 System.out.println("图片开始下载:"); 15 for(int i=0;i<50;i++){ 16 System.out.println("图片下载%"+i); 17 try { 18 Thread.sleep(10); 19 } catch (InterruptedException e) { 20 // TODO Auto-generated catch block 21 e.printStackTrace(); 22 } 23 } 24 System.out.println("图片下载完毕"); 25 isFinish=true; 26 27 //通知显示线程可以开始工作了 28 synchronized (obj) { 29 //obj.notifyAll(); 随机选择线程 解除阻塞 30 obj.notify(); 31 } 32 System.out.println("附件开始下载:"); 33 for(int i=0;i<50;i++){ 34 System.out.println("附件下载%"+i); 35 try { 36 Thread.sleep(10); 37 } catch (InterruptedException e) { 38 // TODO Auto-generated catch block 39 e.printStackTrace(); 40 } 41 } 42 System.out.println("附件下载完毕"); 43 } 44 }; 45 46 Thread show =new Thread(){ 47 @Override 48 public void run() { 49 System.out.println("开始显示图片"); 50 // try { 51 // download.join(); 52 // } catch (InterruptedException e) { 53 // // TODO Auto-generated catch block 54 // e.printStackTrace(); 55 // } 56 //在obj对象上等待 57 try { 58 synchronized (obj) { 59 /* 60 * wait方法要求: 61 * 调用哪个对象的wait的方法 就要将该对象加锁 62 */ 63 obj.wait(); 64 } 65 } catch (InterruptedException e) { 66 // TODO Auto-generated catch block 67 e.printStackTrace(); 68 } 69 70 if(isFinish){ 71 System.out.println("图片显示成功"); 72 }else{System.out.println("图片显示失败");} 73 } 74 }; 75 download.start(); 76 show.start(); 77 78 } 79 }
7. 线程安全API与非线程安全API
之前学习的API中就有设计为线程安全与非线程安全的类:
StringBuffer 是同步的 synchronized append(); 安全的
StringBuilder 不是同步的 append();
相对而言StringBuffer在处理上稍逊于StringBuilder,但是其是线程安全的。当不存在并发时首选应当使用StringBuilder。
同样的:
Vector 和 Hashtable 是线程安全的
ArrayList 和 HashMap则不是线程安全的。
对于集合而言,Collections提供了几个静态方法,可以将集合或Map转换为线程安全的:
例如:
Collections.synchronizedList() :获取线程安全的List集合
Collections.synchronizedMap():获取线程安全的Map
...
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
list = Collections.synchronizedList(list);//将ArrayList转换为线程安全的集合
System.out.println(list);//[A,B,C] 可以看出,原集合中的元素也得以保留
...
/** * 转换线程安全的集合和Map */ class SynCollectionAndMap{ public static void main(String[] args) { //List集合 List<String> list =new ArrayList<String>(); list.add("a"); list.add("b"); list.add("c"); //转换为线程安全的List集合 list =Collections.synchronizedList(list); /* * 能保证:对集合元素进行操作的方法都是同步且 * 互斥的。保证了线程的安全 * 注意:在遍历的过程中,依然可以增删元素 * 解决办法: 对遍历的代码片段加锁,锁的是集合这个对象 */ System.out.println(list); synchronized (list) { java.util.Iterator<String> it = list.iterator(); while(it.hasNext()){ System.out.println(it.next()); } } //Set集合 Set<String> set =new HashSet<String>(); set.add("a"); set.add("b"); set.add("c"); //将Set集合转换为线程安全的 set = Collections.synchronizedSet(set); System.out.println(set); Map<String,Integer> map =new HashMap<String, Integer>(); map.put("张三",22); map.put("赵四",22); map.put("王五",22); //将Map转换为一个线程安全的 map=Collections.synchronizedMap(map); System.out.println(map); } }
8. 使用ExecutorService实现线程池
当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。为此我们应使用线程池来解决这个问题。
ExecutorService是java提供的用于管理线程池的类。
线程池有两个主要作用:
1.控制线程数量
2.重用线程
线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,
服务完后不关闭该线程,而是将该线程还回到线程池中。
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务
线程池有以下几种实现策略:
Executors.newCachedThreadPool()
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
Executors.newFixedThreadPool(int nThreads)
创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
Executors.newScheduledThreadPool(int corePoolSize)
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
Executors.newSingleThreadExecutor()
创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
可以根据实际需求来使用某种线程池。例如,创建一个有固定线程数量的线程池:
...
ExecutorService threadPool
= Executors.newFixedThreadPool(30);//创建具有30个线程的线程池
Runnable r1 = new Runable(){
public void run(){
//线程体
}
};
threadPool.execute(r1);//将任务交给线程池,其会分配空闲线程来运行这个任务。
...
/** * 测试线程池 * @author Administrator * */ class TestThreadPoolDemo{ public static void main(String[] args) { //创建了一个含有10个线程的线程池 ExecutorService threadpool = Executors.newFixedThreadPool(2); for(int i=0;i<5;i++){ Handler handler =new Handler(); threadpool.execute(handler); } System.out.println("任务全部指派完成"); } } /* * 线程要执行的任务 */ class Handler implements Runnable{ @Override public void run() { //获取运行当前任务的线程名字 String name =Thread.currentThread().getName(); System.out.println("运行当前任务的线程是:"+name); for(int i=0;i<10;i++){ System.out.println(name+":"+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("任务完毕"); } }
9. BlockingQueue双缓冲队列
queue 一边进一边出
Deque 两边都能进都能出
BlockingQueue是双缓冲队列、BlockingDeque是双缓冲双端队列
在多线程并发时,若需要使用队列,我们可以使用Queue,但是要解决一个问题就是同步,但同步操作会降低并发对Queue操作的效率。
BlockingQueue内部使用两条队列,可允许两个线程同时向队列一个做存储,一个做取出操作。在保证并发安全的同时提高了队列的存取效率。
双缓冲队列有一下几种实现:
ArrayBlockingDeque:规定大小的BlockingDeque,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的。
LinkedBlockingDeque:大小不定的BlockingDeque,若其构造函数带一个规定大小的参数,生成的BlockingDeque有大小限制,若不带大小参数,所生成的BlockingDeque的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的。
PriorityBlockingDeque:类似于LinkedBlockDeque,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序。
SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。
/** * 双缓冲队列 */ class TestBlockingQueueDemo{ public static void main(String[] args) { /* * 双缓冲队列,创建一个固定长度的,里面存放10个元素 * 该队列是单向的,遵循先进先出的原则 */ final BlockingQueue<Integer> queue =new ArrayBlockingQueue<Integer>(10); /* * 双缓冲双端队列,与单队列的区别在于,队列两端都可以进出队 */ // BlockingDeque<Integer> Deque =new LinkedBlockingDeque<Integer>(10); //向队列中添加元素的线程 Thread offerThread =new Thread(){ @Override public void run() { for(int i=0;i<20;i++){ //局部变量使用前必须初始化 boolean tf = false; try { /* * 该方法允许我们设置一个延迟时间 * 在延迟时间之后还没放入 便返回false */ tf=queue.offer(i,5,TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("添加元素"+i+":"+tf); } } }; offerThread.start(); //从队列中取出元素的线程 Thread pullThread=new Thread(){ @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } for(int i=0;i<20;i++){ int num=0; try { num=queue.poll(5,TimeUnit.SECONDS); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("取出的元素是:"+num); } } }; pullThread.start(); } }