java多线程学习记录(四)

1、原子类:

为什么需要原子类?

对多线程访问同一变量就需要加锁,而锁是比较消耗性能的,JDK1.5之后,新增的原子操作类提供了一种用法简单、性能高效、线程安全的更新一个变量的方式,这些类位于JUC包下的atomic包下,发展到JDK1.8该包下共有17个类,囊括了原子更新基本类型、原子更新数组、原子更新属性、原子更新引用。

原子更新基本类型
AtomicBoolean、AtomicInteger、AtomicLong 元老级原子更新。
DoubleAdder、LongAdder、对Double、Long的原子更新性能进行优化提升
DoubleAccumulator、LongAccumulator 支持自定义运算

import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomic {
    private static AtomicInteger num = new AtomicInteger(0);
    private static void inCrease(){
        num.incrementAndGet();
    }

    public static void main(String[] args) {
        for (int i = 0;i < 10; i++){
            new Thread(()->{
                inCrease();
                System.out.println(num);
            }).start();
        }
    }

}

以上代码在不加锁情况下最后会输出到10,把原来num++通过AtomicInteger的incrementAndGet变成了原子操作。

有关LongAdder及LongAccumulator的自定义运算参考博客:

https://blog.csdn.net/fouy_yun/article/details/77825039

原子更新数组类型:

AtomicIntegArray、AtomicLongArray、AtomicReferenceArray

AtomicIntegerArray使用例子:

public static void main(String[] args) {

    int[] arr = new int[]{3, 2};
    AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(arr);
    System.out.println(atomicIntegerArray.addAndGet(1, 8));
    
    //实现IntBinaryOperator接口的applyASInt方法自定义操作
    int i = atomicIntegerArray.accumulateAndGet(0, 2, new IntBinaryOperator() {
                @Override
                public int applyAsInt(int left, int right) {
                    return left * right / 3;
                }
            }
    );
    //也可用lambda表达式
    int i1 = atomicIntegerArray.accumulateAndGet(0, 2, (left, right) ->
        left * right / 3
    );
    System.out.println(i);
}

原子更新属性:

原子的更新某个类里的某个字段时(不能是static和final修饰的字段),就需要使用原子更新字段类,Atomic包提供了一下四个类进行原子字段更新。

AtomicIntegerFileUpdater、AtomicLongFileUpdater、AtomicStampedReference、AtomicReferenceFileUpdater

需遵循的原则:
1、字段必须是用volatile修饰,在线程之间共享变量时保证立即可见。
2、调用者能够直接操作对象字段,即可以用反射进行原子操作,对于父类的字段子类是不能直接操作的,尽管子类可以访问父类的字段。
3、对于AtomicIntegerFileUpdater和AtomicLongFileUpdater只能修改int/long类型字段,不能修改包装类型(Integer/Long),如果要修改包装类型使用AtomicReferenceFileUpdater。

实例:

import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class AtomicFile {
    public static void main(String[] args) {
        AtomicLongFieldUpdater<Student> atomicLongFieldUpdater = 
                AtomicLongFieldUpdater.newUpdater(Student.class, "id");
        Student student = new Student(1L, "hello");
        atomicLongFieldUpdater.compareAndSet(student, 1L, 100L);
        System.out.println(student.getId());

        AtomicReferenceFieldUpdater<Student, String> name = AtomicReferenceFieldUpdater
                .newUpdater(Student.class, String.class, "name");
        name.compareAndSet(student, "hello","world" );
        System.out.println(student.getName());
    }
}
class Student{
    volatile long id;
    volatile String name;

    public Student(long id, String name){
        this.id = id;
        this.name = name;
    }
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

原子更新引用:

AtomicReference:用于对引用的原子更新。
AtomicMarkableReference:带版本戳的原子引用类型,版本戳为boolean类型。
AtomicStampedReference:带版本戳的原子引用类型,版本戳为int类型。

实例:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        AtomicReference <Stu> atomicReference = new AtomicReference<>();
        Stu stu = new Stu(1L, "hello");
        Stu stu1 = new Stu(2L, "you");
        atomicReference.set(stu);
        atomicReference.compareAndSet(stu, stu1);
        Stu stu2 = atomicReference.get();
        System.out.println(stu2.getName());
    }
}

class Stu{
    private long id;
    private String name;

    public Stu(long id, String name) {
        this.id = id;
        this.name = name;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

2、同步容器与并发容器

collection接口
Collection是最基本的集合接口,声明了适用于JAVA集合(只包括Set和List)的通用方法。Map接口并不是Collection接口的子接口,但是它仍然被看作是Collection框架的一部分。

在这里插入图片描述
collection接口实现
List(interface): List可以通过index知道元素的位置,它允许元素的重复。ArrayList, LinkedList, Vector可以实现List接口。

Set(interface):是不允许元素的重复。HashSet, LinkedHashSet,TreeSet 可以实现Set接口。

Map(interface): 使用键值对(key-value), 值(value)可以重复,键(key)不可以重复。HashMap, LinkedHashMap, Hashtale, TreeMap可以实现Map接口。

collection接口方法
boolean add(Object o) :向集合中加入一个对象的引用

void clear():删除集合中所有的对象,即不再持有这些对象的引用

boolean isEmpty() :判断集合是否为空

boolean contains(Object o) : 判断集合中是否持有特定对象的引用

Iterartor iterator() :返回一个Iterator对象,可以用来遍历集合中的元素

boolean remove(Object o) :从集合中删除一个对象的引用

int size() :返回集合中元素的数目

Object[] toArray() : 返回一个数组,该数组中包括集合中的所有元素

关于:Iterator() 和toArray() 方法都用于集合的所有的元素,前者返回一个Iterator对象,后者返回一个包含集合中所有元素的数组。

在这些容器中同步容器
同步容器可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。

  • Vector
  • Stack
  • HashTable
  • Collections.synchronized方法生成,例如:

Collectinons.synchronizedList()
Collections.synchronizedSet()
Collections.synchronizedMap()
Collections.synchronizedSortedSet()
Collections.synchronizedSortedMap()

其中Vector(同步的ArrayList)和Stack(继承自Vector,先进后出)、HashTable(继承自Dictionary,实现了Map接口)是比较老的容器,Thinking in Java中明确指出,这些容器现在仍然存在于JDK中是为了向以前老版本的程序兼容,在新的程序中不应该在使用。Collections的方法时将非同步的容器包裹生成对应的同步容器。

同步容器在单线程的环境下能够保证线程安全,但是通过synchronized同步方法将访问操作串行化,导致并发环境下效率低下。而且同步容器在多线程环境下的复合操作(迭代、条件运算如没有则添加等)是非线程安全,需要客户端代码来实现加锁。

例如:

public static Object getLast(Vector list) {
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
}

public static void deleteLast(Vector list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
}

上面的代码取最后一个元素或者删除最后一个元素,使用了同步容器Vector。如果有两个线程A,B同时调用上面的两个方法,假设list的大小为10,这里计算得到的lastIndex为9,线程B首先执行了删除操作(多线程之间操作执行的不确定性导致),而后线程A调用了list.get方法,这时就会发生数组越界异常,导致问题的原因就是上面的复合操作不是原子操作,这里可以通过在方法内部额外的使用list对象锁来实现原子操作。

在多线程中使用同步容器,如果使用Iterator迭代容器或使用使用for-each遍历容器,在迭代过程中修改容器会抛出ConcurrentModificationException异常。想要避免出现ConcurrentModificationException,就必须在迭代过程持有容器的锁。但是若容器较大,则迭代的时间也会较长。那么需要访问该容器的其他线程将会长时间等待。从而会极大降低性能。

此外,隐式迭代的情况,如toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都会隐式的Iterate,也可能抛出ConcurrentModificationException。

fail-fast机制:

快速报错机制(fail-fast)能够防止多个进程同时修改同一个容器的内容。如果在你迭代遍历某个容器的过程中,另一个进程接入其中,并且插入、删除或者修改此容器内的某个对象,就会出现问题:也许迭代过程已经处理过容器中的该元素了,也许还没处理,也许在调用size()之后尺寸缩小了等等。fail-fast机制会探查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其他进程修改了容器,立刻抛出ConcurrentModificationException异常,即快速报错——不适用复杂的算法在时候进行检查。

并发容器:

由上面的分析我们知道,同步容器并不能保证多线程安全,而并发容器是针对多个线程并发访问而设计的,在jdk5.0引入了concurrent包,其中提供了很多并发容器,极大的提升同步容器类的性能。

ConcurrentHashMap
对应的非并发容器:HashMap
目标:代替Hashtable、synchronizedMap,支持复合操作
原理:JDK6中采用一种更加细粒度的加锁机制Segment“分段锁”,JDK8中采用CAS无锁算法。
详见:http://blog.csdn.net/u011080472/article/details/51392712

CopyOnWriteArrayList
对应的非并发容器:ArrayList
目标:代替Vector、synchronizedList
原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
详见:http://blog.csdn.net/u011080472/article/details/51419001

CopyOnWriteArraySet
对应的费并发容器:HashSet
目标:代替synchronizedSet
原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
详见:http://blog.csdn.net/u011080472/article/details/51419001

ConcurrentSkipListMap
对应的非并发容器:TreeMap
目标:代替synchronizedSortedMap(TreeMap)
原理:Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过”空间来换取时间”的一个算法。ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上能够在O(log(n))时间内完成查找、插入、删除操作。

ConcurrentSkipListSet
对应的非并发容器:TreeSet
目标:代替synchronizedSortedSet
原理:内部基于ConcurrentSkipListMap实现

ConcurrentLinkedQueue
不会阻塞的队列

对应的非并发容器:Queue
原理:基于链表实现的FIFO队列(LinkedList的并发版本)

LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue
对应的非并发容器:BlockingQueue
特点:拓展了Queue,增加了可阻塞的插入和获取等操作
原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒
实现类:
LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列
ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列
PriorityBlockingQueue:按优先级排序的队列

LinkedBlockingQueue经常用因其可作为生产者消费者的中间商,方法:
队列里存元素:
add():实际调用的offer,区别是在队列满的时候,add会报异常
offer():队列如果满了,直接入队失败
put():在队列满的时候,会进入阻塞的状态。
队列里取元素:
remove():实际调用poll,区别remove会抛出异常,而poll在队列为空时直接返回null。
poll():poll在队列为空时直接返回null
take():在队列为空的时候进入等待状态。

java容器实现类的介绍:

List接口:
LinkedList类
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
   注意:LinkedList是非同步的。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:

List list = Collections.synchronizedList(new LinkedList(…));

ArrayList类
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
   和LinkedList一样,ArrayList也是非同步的(unsynchronized)。一般情况下使用这两个就可以了,因为非同步,所以效率比较高。
   如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。

Vector类
Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该 异常。
  vector的使用主要有如下两种场景:
(1)vector所谓的多线程安全,只是针对单纯地调用某个方法它是有同步机制的。如add,多个线程都在对同一个容器add元素,vector能够保证最后总数是正确的,而ArrayList没有同步机制,就无法保证。
(2)vector的多线程安全,在组合操作时不是线程安全的。比如一个线程先调用vector的size方法得到有10个元素,再调用get(9)方法获取最后一个元素,而另一个线程调用remove(9)方法正好删除了这个元素,那第一个线程就会抛越界异常。
总结:
(1)在需要对容器进行组合操作时,vector不适用(需要自己用synchronized将组合操作进行同步);
(2)仅在上述第一种场景时,才需要使用vector

public class TestMultiThread {
	private static Vector vec = new Vector();
	private static List lst = new ArrayList();
	public void f() {
		TestThread testThread1 = new TestThread();
		TestThread testThread2 = new TestThread();
		Thread thread1 = new Thread(testThread1);
		Thread thread2 = new Thread(testThread2);
		thread1.start();
		thread2.start();
		}	
	public static void main(String[] args) {
		TestMultiThread testMultiThread = new TestMultiThread();
		testMultiThread.f();
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("vec size is " + vec.size());
		System.out.println("lst size is " + lst.size());
	}
	private class TestThread implements Runnable {
		@Override
		public void run() {
			for (int i = 0; i < 1000; ++i) {
				vec.add(i);
				lst.add(i);
			}	
		}		
	}
	private static Vector vec = new Vector();
	private static List lst = new ArrayList();
	public void f() {
		    TestThread testThread1 = new TestThread();
		    TestThread testThread2 = new TestThread();
		    Thread thread1 = new Thread(testThread1);
		    Thread thread2 = new Thread(testThread2);
		    thread1.start();
		    thread2.start();
	} 
}  

如上程序运行结果:

vec size is 2000
lst size is 1999

Stack类
Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有
peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

Set接口:
HashSet类
Java.util.HashSet类实现了Java.util.Set接口。

它不允许出现重复元素;
不保证集合中元素的顺序
允许包含值为null的元素,但最多只能有一个null元素。

public class TestHashSet
{
  public static void main(String [] args)
  {
     HashSet h=new HashSet();
     h.add("1st");
     h.add("2nd");
     h.add(new Integer(3));
     h.add(new Double(4.0));
     h.add("2nd");            //重复元素,未被添加
     h.add(new Integer(3));      //重复元素,未被添加
     h.add(new Date());
     System.out.println("开始:size="+h.size());
     Iterator it=h.iterator();
     while(it.hasNext())
     {
         Object o=it.next();
         System.out.println(o);
     }

     h.remove("2nd");
     System.out.println("移除元素后:size="+h.size());
     System.out.println(h);
  }
}

TreeSet
TreeSet描述的是Set的一种变体——可以实现排序等功能的集合,它在讲对象元素添加到集合中时会自动按照某种比较规则将其插入到有序的对象序列中,并保证该集合元素组成的读优先序列时刻按照“升序”排列。

public class TestTreeSet
{
    public static void main(String [] args)
    {
       TreeSet ts=new TreeSet();
       ts.add("orange");
       ts.add("apple");
       ts.add("banana");
       ts.add("grape");
        Iterator it=ts.iterator();
       while(it.hasNext())
       {
           String fruit=(String)it.next();
           System.out.println(fruit);
       }
    }
}

Map集合接口:
Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。
主要方法:
boolean equals(Object o)比较对象
boolean remove(Object o)删除一个对象
put(Object key,Object value)添加key和value

Hashtable类
Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。添加数据使用put(key,value),取出数据使用get(key),这两个基本操作的时间开销为常数。Hashtable通过initialcapacity和load factor两个参数调整性能。通常缺省的load factor0.75较好地实现了时间和空间的均衡。增大loadfactor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。
  由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希表的操作。
  如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。

HashMap类
HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者loadfactor过低。

  • JDK1.0引入了第一个关联的集合类HashTable,它是线程安全的。 HashTable的所有方法都是同步的。
  • JDK2.0引入了HashMap,它提供了一个不同步的基类和一个同步的包装器synchronizedMap。synchronizedMap被称为有条件的线程安全类。
  • JDK5.0util.concurrent包中引入对Map线程安全的实现ConcurrentHashMap,比起synchronizedMap,它提供了更高的灵活性。同时进行的读和写操作都可以并发地。

HashTable和HashMap区别

  • 第一、继承不同。   
    public class Hashtable extends Dictionary implements Map
    public class HashMap extends AbstractMap implements Map
  • 第二、Hashtable
    中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。
  • 第三、Hashtable中,key和value都不允许出现null值。在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示
    HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,
    而应该用containsKey()方法来判断。
  • 第四、两个遍历方式的内部实现上不同。Hashtable、HashMap都使用了
    Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
  • 第五、哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
  • 第六、Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是
    old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值