并发编程学习

来源:互联网 发布:自动生成声音软件 编辑:程序博客网 时间:2024/06/04 23:43
1,java内存模型(JMM):主内存与工作内存:主内存存储了所有变量,每条线程有自己的工作内存,工作内存保存在被线程使用的变量和主内存变量的副本,线程操作必须在工作内存中进行,不能直接读取主内存而线程间的值传递需要主内存。
,内存操作有8条语句均是原子的。
2,线程同步的方法(多个线程对共享数据的竞争是线程不安全的因素)
线程同步总体可分为有锁同步和无锁同步。有锁同步就是加锁,主要包括synchronized,lock,信号量;无锁同步主要包括CAS,MVCC(参见数据库),volatile,final等方式。
1) synchronized关键字(修饰方法或代码段):
        第一点:synchronized用来标识一个普通方法时,表示一个线程要执行该方法,必须取得该方法所在的对象的锁。
        第二点:synchronized用来标识一个静态方法时,表示一个线程要执行该方法,必须获得该方法所在的类的类锁。
        第三点:synchronized修饰一个代码块。类似这样:synchronized(obj.class) { //code.... }或者synchronized(this) { //code.... }。表示一个线程要执行该代码块,必须获得类锁或对象锁。这样做的目的是减小锁的粒度。对象锁只适用于当前对象的动态方法,类锁是锁住该类的class对象,锁住该类的所有静态方法。
并发编程学习 - Garfield - 张广辉的博客
 对象锁,方法一和方法二只能执行一个

并发编程学习 - Garfield - 张广辉的博客
 类锁 该类所有对象的静态方法只能执行一个

注意:1,在使用synchronized块来同步方法时,非静态方法可以通过this来同步,而静态方法必须使用class对象来同步,但是非静态方法也可以通过使用class来同步静态方法。但是静态方法中不能使用this来同步非静态方法。这点在使用synchronized块需要注意。
2,
所以一个对象内同时存在加锁的动态方法和静态方法时,可以被并发访问。
 
  wait方法:
        该方法属于Object的方法,wait方法的作用是使得调用wait方法的对象所在的当前线程等待,并释放当前线程获得锁,并在其他线程调用notify或者notifyAll方法时恢复到竞争锁状态(一旦获得锁就恢复执行)。
        调用wait方法需要注意几点:
        第一点:wait被调用的时候必须在拥有锁(即synchronized修饰的)的代码块中。
        第二点:恢复执行后,从wait的下一条语句开始执行,因而wait方法总是应当在while循环中调用,以免出现恢复执行后继续执行的条件不满足却继续执行的情况。
        第三点:若wait方法参数中带时间,则除了notify和notifyAll被调用能激活处于wait状态(等待状态)的线程进入锁竞争外,在其他线程中interrupt它或者参数时间到了之后,该线程也将被激活到竞争状态。
        第四点:wait方法被调用的线程必须获得之前执行到wait时释放掉的锁重新获得才能够恢复执行。

    notify方法和notifyAll方法:
        notify方法通知调用了wait方法,但是尚未激活的一个线程进入线程调度队列(即进入锁竞争),注意不是立即执行。并且具体是哪一个线程不能保证。另外一点就是被唤醒的这个线程一定是在等待wait时所释放的锁。
        notifyAll方法则唤醒所有处于wait的线程。
生产者消费者问题

2) 原子操作,对原子变量的操作,使用一条字节码指令即可完成,所以不需要其他同步机制保护:
        第一点:对除了long和double之外的基础数据类型变量进行一次读写。
        第二点:轻量级同步方式volatile。作用:可见性:volatile保证被修饰变量被修改后对其他线程即时可见(立刻同步到主内存);还可以防止被修饰代码段的重排序(JIT)。但是只使用volatile并不能保证线程安全,需要满足一定条件,或者与锁一同使用(如单例模式的双锁校验)
对于初始化一个不可变的引用或基础变量,用final或者volatile可以保证线程安全,当然前提是此变量今后不可变。
        第三点:util.concurrent.atomic包中的原子类,其所提供的方法均为原理操作,原理均采用CompareAndSwap(CAS乐观锁机制,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。则变量改变成功(存在ABA漏洞))。
然后来看看++i是怎么做到的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

首先可以看到他是通过一个无限循环直到increment成功为止.  
循环的内容是
1.取得当前值
2.计算+1后的值
3.如果当前值还有效的话设置那个+1后的值
4.如果设置没成功(当前值已经无效了即被别的线程改过了), 再从1开始.
CAS方法的本质是操作系统底层原语实现,并非java实现。

3)在java.util.concurrent.lock包中的锁类(不是关键字),Lock接口(lock(),unlock(),trylock()方法)和ReentrantLock(重入锁)实现类,读写分离的重入锁ReadWriteLock接口和ReentrantReadWriteLock实现类,类似于数据库的共享锁和排它锁
所有重入锁类中都有一个fair关键字,当设为ture时,如果有大量的锁在等待,将选择一个等待时间最长的锁来执行,这是公平机制。
用lock.newCondition()可以产生await(),signal(),signalAll()方法,类似于wait()和notify().

一个对象中有多个Lock对象的问题:sync中相当于只有1个锁,当有多个lock对象的时候,相当于有多把锁,各个lock之间的锁不互斥。

总结来说,Lock和synchronized有以下几点不同:

  1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直阻塞,不能够响应中断;lock.trylock()能够避免死锁;

  4)lock可以实现线程公平,但会使性能下降。

  5)Lock的读写分离,因为读之间并不会线程不安全,维持多个读操作并发或者一个写操作,但读写之间互斥,类似于共享锁和排它锁。ReentrantReadWriteLock类中有两个成员变量ReadLock和WriteLock。

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

 实现原理对比:

Lock:AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列(双向链表)容纳所有的阻塞线程,而对该队列的操作均通过CAS操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁(曾经获得过该锁的对象更容易获得这个锁)的功能。 

 synchronized 的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁(线程在进入阻塞队列之前,先尝试请求几次锁,无效后进入阻塞队列),并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。 

自旋锁:http://ifeve.com/java_lock_see1/

可重入锁的概念:http://ifeve.com/java_lock_see4/

ASQ同步器:双向链表构建的fifo

http://ifeve.com/introduce-abstractqueuedsynchronizer/

偏向锁:偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它提高了单线程访问同步资源的性能。但试想一下,如果你的同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的。事实上,消除偏向锁的开销还是蛮大的。


线程同步辅助类
4)java.util.concurrent中的信号量Semaphore信号量通常用来限制同时访问共享资源的线程数目。
包括acquire(),release(),tryacquire()。当获取为>0时可以访问,信号减1,等于0时线程阻塞;释放后信号加1。初始化时构造函数传入整数,表示允许同时访问的线程数。Semaphore s = new Semaphore(3);
并发编程学习 - Garfield - 张广辉的博客
 

5)以下三个类并不能用来保护共享资源,而是协调多个线程的运行。

CountDownLatch作用是使某一个线程等待其他线程
在某一个线程内设定初始值,表示等待的线程数;该线程调用await()方法,等待其他线程完成;然后其他线程调用等待线程的countDown(), 每个线程完成使计数器减1.计数器减到0时,等待线程开始执行。如等待所有人都到来后开始开会。

CyclicBarrier的功能是让多个线程相互之间等待
当某个线程执行过程中到达某个点时,调用await(),进入休眠状态,阻塞等待其他线程,当数量足够的线程到达这个点后,唤醒所有线程。他与CountDownLatch有个不同是他的初始值可以被重置,重置后正在阻塞的线程将会被异常所中断。注意在使用时只new一个该对象。

java7开始在concurrent包中加入了Phaser类,Phaser功能更为强大,他可以将并发的代码分为多阶段,在每一阶段结束时对线程进行同步,当所有线程都完成了这一阶段,在开始下一个阶段。arriveAndAwaitAdvance()实现线程阻塞,用来分阶段。每当状态切换时,会自动调用onAdvance()函数,因此可以通过该函数控制每个阶段执行哪些操作。比上两个类的优点在于动态的增减任务数。
6)Exchanger可以实现定义某一个时间点并发线程间交换数据,来实现同步。在生产者消费者实例中,生产者和消费者有各自的容器,他们会在每次生产完东西之前和每次消费完东西之后进行一下容器的同步。


 

并发编程基础知识总结

1,线程创建的两种方式:继承Thread类,重写run方法,Thread t1=new ...,t.start();

实现runnable接口,Thread t2 = new Thread(new runnable),t.start();

2,每个线程有自己的ID,name,优先级,状态(new,runnable,blocked(阻塞),waiting(无限期等待,wait(),join(),),time waiting(有限期等待,wait(),join(),sleep),terminated);

3,线程终断控制的方式:调用interrupt()线程中断函数使某线程中断后,可以通过调用t.isInterrupt()检测或者Thread.interrpted()检测,然后可以抛出InterrputedException传递这个中断信息。

4,所谓的阻塞,就是线程能够运行,但是某个条件阻止它的运行,当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间,直到线程重新进入就绪状态,它才有可能执行操作。就绪并代表是在运行啊,所谓的就绪,就是可运行也可不运行,只要调度器分配时间片给线程,线程就可以运行,因为我们都知道,调度器是如何分配线程,是不确定的。它与wait的区别在于blocked是等待获取锁,而wait是等待一段时间或者等待被唤醒。
       

5,sleep()方法让线程睡眠,不占用CPU,让低优先级线程执行。但与wait的不同在于sleep()期间始终占着锁。而wait不占锁。

yield()方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。

6,join()与wait()的区别在于wait()让自己等别人,join让别的线程等自己。例如:在主线程中调用t.Join(),也就是在主线程中加入了t线程的代码,必须让t线程执行完毕之后,主线程(调用方)才能正常执行。

7,守护线程是一个优先级最低的线程,当他运行结束时,整个程序也就结束了。他常作为其他线程的服务线程,不能做重要的事儿

8,线程异常处理:java的异常分为非运行时和运行时异常,非运行时需要抛出和捕获(IOex,classnotfound),运行时不需要(numberformat)。run方法不支持异常抛出,需要特殊处理。

9,继承了Thread或者实现了runnable接口的类的一个对象,如果被多个线程并发,成员变量是线程共享的,如果不想共享,可通过Threadlocal声明自己线程的私有变量。

http://blog.csdn.net/lufeng20/article/details/24314381

10,可以通过实现ThreadFactory创建线程工厂,统一new线程。执行器框架和forkjoin框架用该方式建立线程,只不过执行器框架中所创建的是普通Thread,而forkjoin中创建的是工作者ForkJoinWorkerThread。这些类都可以被继承实现自己的线程类,但同时要实现工厂类。如果实现了自己的工厂,初始化框架的线程池时当参数传入即可。

11,进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,比如在Windows系统中,一个运行的exe就是一个进程。

线程是指进程中的一个执行流程,cpu调度的基本单位,虽然共享一块内存空间,但可以独立调度,独立的栈,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。

12、线程池:好处:线程复用,减少了创建和销毁线程的时间。

1),执行器框架(Executor Framework)
ThreadPoolExecutor ex = (ThreadPoolExecutor)Executors.newCachedThreadPool(); (Executors的静态方法创建线程池,也可直接new)
ex.execute(task);    放入实现了Runnable或者Callable接口的对象执行线程。
ex.shutdown();    调用该方法后,线程池执行完所有任务,,线程销毁,线程池关闭

当使用Executors.newFixedThreadPool(num)时,创建固定数目的线程,多余的任务会被阻塞直到有空余线程。

Executors.newScheduledThreadPool(num)该执行器可以延时或者周期性的执行任务(ThreadPoolExecutor的子类)。

Callable和Future,Callable是一个类似Runnable的接口,用线程池执行任务时,可以用它
Future<> rs = ex.submit(callable);

Runnable和Callable的区别是,
(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

invokeAll方法处理任务列表。
List<Future<>> rs = ex.invokeAll(List<Callable<>>);


2),Fork/Join 框架(使用分治策略,将主任务分为多个小任务,jdk7)
与执行器框架的不同在于工作窃取算法。主任务等待他所创建子任务完成,执行这个任务的线程为工作者线程(ForkJoinWorkerThread类,thread的子类),工作者线程寻找仍未被执行的任务,并在子线程执行时间里利用所有线程优势(其实就是分治策略)。 
 任务继承ForkJoinTask(ForkJoinTask是个抽象类,它有两个实现类RecursiveTask和RecursiveAction前者用于不返回结果,后者用于返回结果,类似Callable和Runnable。但如果传入Runnable或者Callable,就不会使用工作窃取算法了),实现compute()(类似Run()),如果主任务范围太大,new两个范围小的子任务,然后使用invokeAll(t1,t2)进行分治。在主函数中用ForkJoinPool执行主任务。

task extends RecursiveAction {
         int start,end
         compute() {
                  if(end-start<num){
                   }
                  else {
                        task1 = new(midlle,end); 
                        task2 = new(start,midlle); 
                        invokeAll(task1,task2);
                 }
         }
}

main() {
         ForkJoinPool   pool  =  new ForkJoinPool();
         pool.excute(task);
         pool.shutdown();
}

任务可以继承RecursiveTask(ForkJoinTask的子类)通过任务的get(),获得compute()的返回值,然后将子任务的返回值进行合并,作为主任务的返回值返回。

task extends RecursiveTask<>{
         int start,end
         compute() {
                  if(end-start<num){
                       return   
                   }
                  else {
                        task1 = new(midlle,end); 
                        task2 = new(start,midlle); 
                        invokeAll(task1,task2);
                        return mergeRs(task1.get(),task2.get())           //这是自定义的方法
                 }
         }
}

同步和异步执行:当用invokeAll(t1,t2)时,必须等到任务执行完毕后,方法返回;当采用fork(t)发送任务(递归),发送任务完成后立即返回,继续执行其他,然后在调用join()方法,等待子任务完成后将结果合并,并返回结果。

task extends RecursiveTask<>{
         int start,end
         compute() {
                  if(end-start<num){
                       return   
                   }
                  else {
                        task1 = new(midlle,end); 
                        task2 = new(start,midlle); 
                        task1.fork();
                       task2.fork();
                        return mergeRs(task1.join(),task2.join());    
                 }
         }
}

compute()中可以抛出异常,调用task中的方法判断是否有异常和异常的类型,但异常抛出后程序不会停止。
任务在执行之前可以被取消。

14,并发集合(线程安全)
1)阻塞式集合:当集合满或者空时,被调用添加或者删除方法的线程被阻塞,直到方法能够被成功执行。阻塞队列接口和阻塞栈接口.实现原理,事实它和我们用Object.wait()、Object.notify()和非阻塞队列实现生产者-消费者的思路类似.有了这样的功能,就为多线程的排队等候的模型实现开辟了便捷通道,在线程池初始化时传入一个阻塞队列,存放任务。

BlockingQueue:BlockingQueue是阻塞队列的总接口,它的大致成员包括ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue和java7引进的TransferQueue。

ArrayBlockingQueue,LinkedBlockingQueue:分别维护一个数组和链表作为数据缓冲区。在线程同步方面,ArrayBlockingQueue中的生产和消费用一个锁,所以生产和消费不能同时进行,而LinkedBlockingQueue是两个锁。

SynchronousQueue:没有数据缓冲区,一个插入和一个删除必需交替进行。使用两个队列(一个用于正在等待的生产者、另一个用于正在等待的消费者)和一个用来保护两个队列的重入锁。

http://ifeve.com/java-synchronousqueue/

http://wsmajunfeng.iteye.com/blog/1629352/

TransferQueue:是上述阻塞队列的超集,CAS实现锁同步。

http://ifeve.com/java-transfer-queue/


2)非阻塞集合:。。。。。。。。。。。。。。。。。。。。线程不会被阻塞,方法会返回null或者异常。

3)并发集合实现原理:
ConcurrentHashMap:http://www.iteye.com/topic/344876(重点

 基本思路:并发的HashMap利用Sync将Hash表中的各个方法进行加锁,包括读和写方法。这样相当于对整张Hash表加锁,因此这样不但读写互斥,读和读之间也会互斥。

ConcurrentHashMap将整个哈希表分成了多个Segment,即多个小的Hash表,每个Segment单独加锁,这叫锁分离技术。数据存入时,先Hash定位到segment,在Hash存入之中。

写操作(put和remove)需要加锁,put一个节点采用头插法;remove节点时,由于各个节点除了val都是final,所以需要将删除节点之前的节点复制后从新指向。

读操作(get)不需要加锁第一步是访问count变量(记录每个segment键值对的数量),如果是0就不读了。最后找到get的值时,用return readValueUnderLock(e);在加锁方式下检查一遍读出的值,以避免在读的过程中其他线程修改了这个值。

size方法对所有segment中元素数量求和,先在无锁条件下执行,如果失败,再在有锁条件下执行,判断失败的条件是利用modCount和CAS算法。

这样处理使得ConcurrentHashMap读和读不互斥,读和写也不互斥。

  • CopyOnWriteArrayList:通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
  • 写数据时加锁,否则会copy出多个副本出来;读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
  • 这样实现读写不互斥,读和读也不互斥。但不能保证数据的强一致性。
  • http://www.cnblogs.com/dolphin0520/p/3938914.html


17,死锁问题
如何避免死锁:java中,增大锁的粒度,Lock中的tryAcquire
死锁实现(会写):注意此处Object前加上static的原因是让所有的TestDeadLock类共享同一个o1和o2对象,因为在main中new了两个TestDeadLock。如果不写static,每个TestDeadLock对象都会有自己的o1和o2. 这里并不是类锁哦。
并发编程学习 - Garfield - 张广辉的博客
 

18,生产者消费者写法(会写
并发编程学习 - Garfield - 张广辉的博客
 
并发编程学习 - Garfield - 张广辉的博客
 
并发编程学习 - Garfield - 张广辉的博客
 
19,锁优化技术:
由于线程的创建和销毁,线程间的切换都需要消耗巨大的资源,所以:
1)减小锁的持有时间;
2)采用所分离技术,如并发HashMap;
3)采用非独占锁(共享锁)和非阻塞锁(乐观锁)实现。

0 0
原创粉丝点击