4)一些常用的线程安全的容器,及使用注意事项。如:ConcurrentHashMap,Blockingqueue及其子类等。
synchronized有两种使用方式:一种是synchronized方法、一种是synchronized块。但无论哪一种方式,都是同一个核心思想:Synchronized锁是针对对象实例的,也就是jvm一个对象实例对应的一块内存地址。
因此:一个包含有同步块或同步方法类,如果有多个实例,这多个实例无同步关系。
一个类中包含多个同步方法A、B、C,对于同一对象实例O而言,多个同步方法是互斥的。如:线程1调用方法A时,线程2想调用方法B或C,需要等待线程1执行完方法A,释放对象O的锁。
在线程类中创建同步方法,证实不同的线程之间,同步方法无效,因为每个线程运行时,都是不同的实例。(由此可见,一般不赞成线程里面创建静态方法。)
如果将同步方法改成静态同步方法,那么无论普通对象还是线程对象,所有线程都会对此同步方法的调用同步。因为此同步静态方法在内存中的地址唯一,我们为此段内存加了锁。
假设有一个账户类,省略属性,有存钱、取钱两个方法。这两个方法需要互斥:不能同时存钱、不能同时取钱、存钱同时不能取钱、取钱同时不能存钱。
1)下面是一个AccountObj对象,将deposit()和withdraw()方法都加上synchronized关键字。
public class AccountObj {
//省略属性...
public synchronized void deposit(int amt){
System.out.println("despsit start-----");
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("despsit end-----");
}
public synchronized void withdraw(int amt){
System.out.println("withdraw start#####");
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("withdraw end#####");
}
}
public class Client {
public static void main(String[] args){
final AccountObj aco = new AccountObj();
new Thread(){ //线程1,调用deposit()方法
public void run(){
aco.deposit(10);
}
}.start();
new Thread(){ //线程2,调用withdraw()方法
public void run(){
aco.withdraw(10);
}
}.start();
}
}
despsit start-----
despsit end-----
withdraw start#####
withdraw end#####
上面的实验测试了一个对象的多个同步方法在多线程中依然是同步的。那么如果一个类的多个对象,其同步方法还能同步吗?
1)实时上面的例子,修改测试客户端如下:
public class Client2 {
public static void main(String[] args) {
//包含同步方法的实例1
final AccountObj ao1 = new AccountObj();
//包含同步方法的实例2
final AccountObj ao2 = new AccountObj();
new Thread(){ //线程1,调用实例1的存钱方法
public void run(){
ao1.deposit(10);
}
}.start();
new Thread(){ //线程2,调用实例2的存钱方法
public void run(){
ao2.deposit(10);
}
}.start();
}
}
2)测试输出:
despsit start-----
despsit start-----
despsit end-----
despsit end-----
上面的测试结果就很好的解释了一个账户对存钱和取钱的基本控制。
账户类AccountObj,可以有多个实例,每个实例之间的数据无关,同步无关。
同步控制,只需要加载特定的对象之上。并且,存钱的同时,在其他地方发生了取钱,也必须存取平衡。一个类的存取方法都加上同步关键字就能保证:存存同步、取取同步、存取同步。
1)下面修改AccountObj类,测试此特性:
public class AccountObj {
private int amount;
public AccountObj(int amount){ //账号初始值
this.amount = amount;
}
public synchronized void deposit(int amt){
int tmp = amount;
tmp += amt;
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
amount = tmp;
}
public synchronized void withdraw(int amt){
int tmp = amount;
tmp -= amt;
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
amount = tmp;
}
public int getAcount(){
return amount;
}
}
public class Client {
//模拟存取次数1000次
private static int THREAD_MOCK_NUM = 1000;
static Thread[] threadsHold = new Thread[THREAD_MOCK_NUM];
public static void main(String[] args){
//初始化账号有1000块钱
final AccountObj aco = new AccountObj(1000);
for(int i=0;i
3)测试结果:多次测试,收支平衡,输出结果恒为:账号余额:1000
但如果将AccountObj存取方法的synchronized关键字去掉,则每次输出的结果就不一定了。
(对于在一个线程类中创建同步方法是无用之举,应为不会各个线程之间不会发生同步。对于这句话就不贴代码举例了)
同步块的语法:synchronized(obj){//java代码块}。
解释:synchronized块在执行前,先要获得对象obj的对象锁,如果obj对象锁可用,则当前线程独占obj锁,直到synchronized块执行完毕,释放锁,则其他线程可争用此锁,以执行同步块。
注意,这里的obj是实例对象,也就是说:
1)如果两个线程,加锁的obj,是同一个实例对象,则同步块可以进行同步;两个线程的加锁obj是两个不同的实例,那么同步块无效的,因为线程获得的是不同对象上的锁,并不会互斥。因为,同步是针对对象。
2)对于所有的同步块的使用,只要注意锁机制是发生在一个相同的对象实例上即可。无论同步的是对象,this,还是Object.class,只要理解了实例的概念就能很好的控制。如果对一个单例对象加锁,那么所有线程在此同步块访问时都会同步,与Object.class一样,因为每个class在内存中也只有一个对应的Class对象。
3)当我们在使用同步块时,注意力应该放在同步对象obj上。比如3个不同的同步块X、Y、Z,针对同一个对象同步,实际上这三个同步块的线程争用时是互斥的。而不仅仅是X-X争用同步、Y-Y争用同步、Z-Z争用同步等。
1)假设我们有一个类:ObjectDemo.java。
2)有一个线程例子:
package com.thread.syncBlock;
public class ThreadDemo implements Runnable {
private String method;
public ThreadDemo(String method){
this.method = method;
}
@Override
public void run() {
if("1".equals(method)){
processSync1();
}
if("2".equals(method)){
processSync2();
}
}
private void processSync1(){
synchronized(ObjectDemo.class){ //对一个加载类加锁,同步块X
System.out.println(Thread.currentThread().getName()+"--进入方法1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--退出方法1");
}
}
private void processSync2(){
synchronized(ObjectDemo.class){ //对同一个加载类加锁,同步块Y
System.out.println(Thread.currentThread().getName()+"--进入方法2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--退出方法2");
}
}
}
package com.thread.syncBlock;
public class SyncBlockTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadDemo("1"));
t1.start();
Thread t2 = new Thread(new ThreadDemo("2"));
t2.start();
}
}
Thread-0--进入方法1
Thread-0--退出方法1
Thread-1--进入方法2
Thread-1--退出方法2
同步方法执行时,是在对象实例本身之上加锁;而同步块我们可以自定义需要加锁的对象,如果用this关键字:synchronized(this){..},那么锁也是加在自身实例之上。我们需要注意控制的是同步块中的对象,确实是符合业务逻辑的同步对象。
换句话说:静态同步方法、静态同步变量、synchronized(AnyObject.class)、synchronized(“”)等都有同样的效果,对所有调用此段代码的所有线程实现同步。
wait和notify方法的调用需要在同步关键字synchronized中使用,如:shnchronized(obj){obj.wait;}和synchronized(obj){obj.notify}。
object的wait和notify方法可以用来协调控制,多线程对同一对象的操作问题。
使用场景:我的逻辑需要A线程获取一个值对象Obj,但这个值需要调用B线程生产,但因为网络原因、操作时长不定,我们需要线程A、B之间对值对象Obj存取同步,那么wait、notify就很合适。
package com.thread.waitNotify;
public class ObjectInfo {
private String name;
private String desc;
public ObjectInfo(){}
public ObjectInfo(String name,String desc){
this.name = name;
this.desc = desc;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String toString(){
return "{name:"+name+";desc:"+desc+"}";
}
}
package com.thread.waitNotify;
public class ThreadWait implements Runnable {
private Object lock;
public ThreadWait(Object lock){
this.lock = lock;
}
@Override
public void run() {
synchronized(lock){
System.out.println(WaitNotifyTest.getNowTime()+"\t"+Thread.currentThread().getName()+"执行下发,等待返回……");
try {
lock.wait();3)
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(WaitNotifyTest.getNowTime()+"\t"+Thread.currentThread().getName()+"跳出wait----");
ObjectInfo oi = (ObjectInfo)lock;
System.out.println(oi.toString());
}
}
}
3)一个处理返回值的线程,返回值处理成功后,唤醒等待线程:
package com.thread.waitNotify;
import com.thread.Util;
public class ThreadNotify implements Runnable {
public Object lock;
public ThreadNotify(Object lock){
this.lock = lock;
}
public void run() {
synchronized(lock){
System.out.println(Util.getNowTime()+"\t"+Thread.currentThread().getName()+"进入信息回执处理");
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ObjectInfo oi = (ObjectInfo)lock;
oi.setName("测试");
oi.setDesc("回执");
lock.notify();
}
}
}
4)客户端代码:
package com.thread.waitNotify;
import java.text.SimpleDateFormat;
import java.util.Date;
public class WaitNotifyTest {
public static void main(String[] args) {
ObjectInfo info = new ObjectInfo();
Thread tw = new Thread(new ThreadWait(info)); //tw线程获取值时,进入wait等待
tw.start();
Thread tn = new Thread(new ThreadNotify(info)); //tn线程获取返回值,唤醒wait线程
tn.start();
}
public static String getNowTime(){
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
return sdf.format(date);
}
}
5)测试结果输出:
20150323 14:06:33 Thread-0执行下发,等待返回……
20150323 14:06:33 Thread-1进入信息回执处理
20150323 14:06:43 Thread-0跳出wait----
{name:测试;desc:回执}
当thread-0(tw)进入休眠后,thread-1(tn)将对象info赋值,然后唤醒thread-0,thread-0进行往下执行,输出了info的信息。
结果也同时说明,当调用了obj.wait()方法,obj的锁能够被释放,为其他线程所用。
package com.thread.lock.rwlock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//内存容器类
public class Config {
private static final ReadWriteLock lock = new ReentrantReadWriteLock();
private static final Lock rlock = lock.readLock(); //从读写锁获取读锁
private static final Lock wlock = lock.writeLock(); //从读写锁获取写锁
//数据库字典表缓存模拟容器
private static Map cache = new HashMap();
//读操作1
public static Map getMap(){
try{
rlock.lock(); //加读锁
return cache;
}finally{
rlock.unlock(); //读锁释放,注意释放锁放在finally块中
}
}
//读操作2
public static String getValueByKey(String key){
try{
rlock.lock();
return cache.get(key);
}finally{
rlock.unlock();
}
}
//写操作,刷新
public static void refreah(){
wlock.lock(); //加写锁
System.out.println(Test.getNowTime()+Thread.currentThread().getName()+"--写线程执行开始");
try{
//刷新模拟
cache.clear();
try {
Thread.sleep(5*1000); //模拟数据库操作耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=1;i<20;i++){
cache.put(String.valueOf(i), String.valueOf(Math.random()));
}
}finally{
System.out.println(Test.getNowTime()+Thread.currentThread().getName()+"--写线程执行结束");
wlock.unlock(); //释放写锁,在finally块中执行
}
}
}
package com.thread.lock.rwlock;
public class WriteThread implements Runnable {
@Override
public void run() {
Config.refreah();
}
}
package com.thread.lock.rwlock;
public class ReadThread implements Runnable {
@Override
public void run() {
System.out.println(Test.getNowTime()+Thread.currentThread().getName()+"++当前缓存中特定key的值为:key=12 value="+Config.getValueByKey("12"));
}
}
package com.thread.lock.rwlock;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test {
public static void main(String[] args) {
Config.refreah(); //先将数据加载到缓存
Thread rt1 = new Thread(new ReadThread()); //第一次取值
rt1.start();
try {
Thread.sleep(10); //给rt1留点锁争用时间,免得wt1比rt1获取锁的时间还快
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread wt1 = new Thread(new WriteThread()); //第一次刷新
wt1.start();
try {
Thread.sleep(100); //确保wt1获取到锁,然后rt2来读数据,会等待读锁的释放才能取值
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread rt2 = new Thread(new ReadThread()); //第二次取值
rt2.start();
}
public static String getNowTime(){
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(date)+" ";
}
}
14:24:42 main--写线程执行开始
14:24:47 main--写线程执行结束
14:24:47 Thread-0++当前缓存中特定key的值为:key=12 value=0.7770040453144604 //原数据
14:24:47 Thread-1--写线程执行开始
14:24:52 Thread-1--写线程执行结束
14:24:47 Thread-2++当前缓存中特定key的值为:key=12 value=0.8918793843441799 //新数据,此数据已被刷新
6)测试说明
1)ConcurrentHashMap的同步机制用到的不仅仅是一个锁,其中包含segment数组,每个segment就是一个ReentrantLock锁。当我们需要对ConcurrentHashMap就行put、get操作时,首先会定位到要操作map的某个segment,并获取segment锁,而其他段完全不受此段锁的影响。这要提高了吞吐量。
2)上面的put、get方法等是局部操作,但有一些整体操作如size、clear等就可能需要获取全部分分段锁,然后一一处理。
3)ConcurrentHashMap可能出错的情况在于迭代,当我们获取了keySet,然后开始迭代时,可能其他线程修改过map,有可能有些key就取不到值,而造成异常。
4)使用ConcurrentHashMap时,要具体情况具体考虑。因为map的同步颗粒度非常小,是在map内部完成的。此map像读写锁的功能就不能实现,因为刷新要两步:(1)clear();(2)循环put()。用ConcurrentHashMap来做的话,这两步是被分别同步的,期间有操作间隙可供其他线程利用,此间隙就可能取出null值来。所以具体问题具体分析。
5)HashMap到底不安全在什么地方?主要出在HashMap的扩容上。比如多线程多对HashMap进行put,当达到某一阀值,HashMap就会自动扩容,若果扩容期间发生存取,HashMap内部索引的计算就可能会发生异常。
参考文档:http://www.cnblogs.com/jackyuj/archive/2010/11/24/1886553.html
这是一个链式阻塞队列,是“生产者-消费者”模式中非常常用的一个容器,他们字段阻塞速率不一致的生产端或消费端。经测试,此容器会阻塞的方法有:
会阻塞的取方法:take()和poll(long timeout,TimeUnit unit),注意无参poll()方法不会阻塞
会阻塞的入方法:put()和offer(E obj,long timeout,TimeUnit unit),无参offer()方法不会阻塞
add()会抛异常,queue full;
ArrayBlockingQueue和LinkedBlockingQueue都是阻塞队列。但从名称上可以看出根本区别,在于数据结构,前者是数组,后者是链表。其方法和功能上大同小异,都用于控制典型的“生产者-消费者”线程同步。区别在于前者初始化必须指定容量,而且超出容量会报错;前者可以指定自己的锁是公平锁还是非公平锁;前者存取两端用的是同一把锁,存取不能并发,后者的存取是两把锁,互不相关,可以并发存取;前者在添加对象的时候,不会生成额外的对象,后者在添加对象时,会生成一个node对象,对高并发会增加GC的消耗。
对于ArrayBlockingQueue,存取用的是同一把锁,个人有个疑问:当取值阻塞的时候,入值如何获取到同一把锁?查看代码一知半解,但大概知道含义:当取值阻塞的时候,当前线程进入等待,需要释放锁;此后,有写入线程执行时,才能获取此锁,加入数据后中断(interupt)等待的线程。
其中,还有一个并发队列也是作为生产者消费者的首选: ConcurrentLinkedQueue ,它是非阻塞队列,肯定就不是出自 Blockingqueue 接口,而是出自 AbstractQueue ,因此也就没有put和take方法,使用这个并发队列需要有两点注意:第一,判断是否为空尽量使用 isEmpty 方法,不要用 size() ,有人测试过 size 方法很耗费时间;第二就是线程问题,虽然 ConcurrentLinkedQueue 是线程安全的,但是只负责原子性的,就是说当你操作 queue.add() or queue.poll 的时候是安全的,当并发量较大时,你在使用 queue.isEmpty 时还不为空,但就在这空当有可能就执行 poll 操作,导致队列为空引起异常,可用如下代码来控制大并发:
synchronized(queue) {
if(!queue.isEmpty()) {
queue.poll();
}
}
参考链接:http://www.cnblogs.com/dolphin0520/p/3938914.html
如果说ArrayList是线程不安全的,那么其对应的线程安全的类就是CopyObWriteArrayList。
CopyOnWriteArrayList实现同步的原理:在写的时候,先将原数组拷贝一份,然后执行修改,修改完后将新对象的索引赋值给变量,就不会造成读值的异常。
此数组有两个缺点:1)数组修改基于拷贝,内存消耗比较大;2)数据实时性可能有点延迟。
个人有个疑问:如果在遍历的时候,如果有其他线程修改过此数组,那变量结果会是怎样?实验结果是:不会影响变量的访问,也不会抛出异常。但当前遍历结果为旧值。代码贴在下面。
1)测试类
package com.thread.copyOnWrite;
import java.util.concurrent.CopyOnWriteArrayList;
public class ListTest {
private static CopyOnWriteArrayList cowList = new CopyOnWriteArrayList();
public static void main(String[] args) {
int size = 0;
while(size++<10){
cowList.add(String.valueOf(size));
}
new Thread(new Runnable(){
@Override
public void run() {
int count = 0;
System.out.println("--begain:"+cowList.size());
for(String li:cowList){
System.out.println(li);
if(count==5){
try {
Thread.sleep(100); //遍历数组时,让线程休眠100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
}
System.out.println("--begain:"+cowList.size());
}
}).start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable(){
@Override
public void run() {
cowList.remove("5"); //在遍历休眠的时候,删除其中的一个数
}
}).start();
}
}
2)输出结果
--begain:10
1
2
3
4
5
6
7
8
9
10
--begain:9
如上,在遍历开始,输出数组大小为10,在遍历过程中,删除数组中的一个对象,遍历结束后, 数组大小编程了9。但细心观察,可发现数组中的输出依然包含“5”。即此数组在遍历中被修改,不会影响原遍历结果。
Vector是一个线程安全的数字。但实际的多线程并发可能很少会用。因为Vector的实现机制很简单,就是继承、实现了list相关的父类、接口,然后重新其中的方法,然后再大部分方法前加了synchronized关键字,如get()、remove()方法等。这样做的结果是什么呢?Vector的一个对象,其中包含10多个同步方法,这些方法在多线程访问时互斥的,即只要有一个线程访问vector对象的任意同步方法,那么此对象的其他同步方法的访问线程均要等待。对于高并发的程序,是要细致考虑的。
HashTable与Vector的实现方式相仿。实际开发几乎没用过hashTable。