线程同步
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();
}
}
