并发编程学习

本文深入探讨了Java内存模型的概念,包括主内存与工作内存的关系,以及线程间数据传递的过程。详细解释了Java中同步的实现方式,包括使用`synchronized`关键字、`wait`和`notify`方法,以及原子操作和并发集合的使用。同时,介绍了线程同步辅助类的作用,如`Semaphore`、`CountDownLatch`、`CyclicBarrier`和`Exchanger`,并总结了Java线程同步的基础知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1,java内存模型:主内存与工作内存:主内存存储了所有变量,每条线程有自己的工作内存,工作内存保存在被线程使用的变量和主内存变量的副本,线程操作必须在工作内存中进行,不能直接读取主内存而线程间的值传递需要主内存。
,内存操作有8条语句均是原子的。
2, 线程同步的方法(多个线程对共享数据的竞争是线程不安全的因素)
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
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方法所在部分(代码块)的线程停止执行,并释放当前获得的调用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保证被修饰变量被修改后对其他线程即时可见(被即可同步到主内存),还可以防止被修饰代码段的重排序。但并不是线程安全,并不能完全的保证原子性,需要满足一定条件,与其他同步方式一同使用 (单例模式的双锁校验)
        第三点:util.con current.atomic包中的原子类,其所提供的方法均为原理操作,原理均采用CompareAndSwap(CAS乐观锁机制,先取得变量的原始值,然后在本地改变变量的值,然后在对比一下变量的原始值是否被其他线程改动了,如果没改动,则变量改变成功(存在ABA漏洞))。

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

总结来说,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可以提高多个线程进行读操作的效率。lock的读写分离,因为读之间并不会线程不安全。

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

 

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

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(....),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()方法让线程睡眠,让低优先级线程执行。但与wait的不同在于sleep()期间始终占着锁。而wait不占锁。

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

6,join()与wait()的区别在于wait()让自己等别人,join让别的线程等自己。,

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

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

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

Object o = new Object(); 

for(int i=0;i<10;i++) {

Thread t = new Thread(o);

t.start();

}

http://blog.youkuaiyun.com/lufeng20/article/details/24314381

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

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

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

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

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

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

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

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

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


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

如果运行的线程少于 corePoolSize,则 Executor始终首选添加新的线程,而不进行排队。(如果当前运行的线程小于corePoolSize,则任务根本不会存放,添加到queue中,而是直接抄家伙(thread)开始运行)

如果运行的线程等于或多于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。

如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

 

排队有三种通用策略:

直接提交。

工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

无界队列。

使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

有界队列。

当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。


非阻塞:。。。。。。。。。。。。。。。。。。。。线程不会被阻塞,方法会返回null或者异常。
优先级队列,放入的每个元素必须继承compareable接口

http://www.ibm.com/developerworks/cn/java/j-jtp07233/

并发集合实现原理:
并发hashMap: http://www.cnblogs.com/yydcdut/p/3959815.html,将整个哈希表分成了16各部分,每个部分有各自的锁,这叫锁分离技术,在调用get()时并不加锁。


SynchronousQueue与java7中的TransferQueue:生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费,阻塞就是发生在元素从一个线程transfer到另一个线程的过程中,它有效地实现了元素在线程之间的传递。

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

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

 

15 线程池 http://www.cnblogs.com/yydcdut/p/3890893.html

好处:线程复用,减少了创建和销毁线程的事件。


16,并发面试题http://www.cnblogs.com/yydcdut/p/3895956.html

17,死锁问题
如何避免死锁:java中,增大锁的粒度,Lock中的tryAcquire
死锁实现( 会写):
并发编程学习 - Garfield - 张广辉的博客
 

18,生产者消费者写法( 会写
http://tanlan.iteye.com/blog/1158154
http://blog.youkuaiyun.com/thinkpadshi/article/details/8163751
参考马士兵视频
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值