精华内容
下载资源
问答
  • 1、并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的...synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,释放之前最新的值刷新到主内存,实现可见性。 3...

    file

    1、并发编程三要素?

    1)原子性

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

    2)可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

    实现可见性的方法:

    synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

    3)有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行

    2、多线程的价值?

    1)发挥多核CPU的优势

    多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

    2)防止阻塞

    从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

    3)便于建模

    这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

    file

    3、创建线程的有哪些方式?

    1)继承Thread类创建线程类

    2)通过Runnable接口创建线程类

    3)通过Callable和Future创建线程

    4.创建线程的三种方式的对比?

    1)采用实现Runnable、Callable接口的方式创建多线程。

    优势是:

    线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势是:

    编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

    2)使用继承Thread类的方式创建多线程

    优势是:

    编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    劣势是:

    线程类已经继承了Thread类,所以不能再继承其他父类。

    3)Runnable和Callable的区别

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

    5、线程的状态流转图

    线程的生命周期及五种基本状态:

    file

    史上最强多线程面试44题和答案:线程锁 线程池 线程同步等

    Java线程具有五中基本状态

    1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

    2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    2.同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    3.其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    6.什么是线程池? 有哪几种创建方式?

    线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

    java 提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

    四种线程池的创建:

    (1)newCachedThreadPool创建一个可缓存线程池

    (2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

    (3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    (4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

    7.线程池的优点?

    1)重用存在的线程,减少对象创建销毁的开销。

    2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

    3)提供定时执行、定期执行、单线程、并发数控制等功能。

    8.Java中的同步集合与并发集合有什么区别?

    同步集合类:

    • Vector
    • Stack
    • HashTable
    • Collections.synchronized方法生成

    并发集合类:

    • ConcurrentHashMap
    • CopyOnWriteArrayList
    • CopyOnWriteArraySet等

    9.同步集合与并发集合的区别

    同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。同步集合比并发集合会慢得多,主要原因是锁,同步集合会对整个May或List加锁,而并发集合例如ConcurrentHashMap,把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段(JDK1.8版本底层加入了红黑树)。

    10.常用的并发工具类有哪些?

    • CountDownLatch
    • CyclicBarrier
    • Semaphore
    • Exchanger

    11.CyclicBarrier和CountDownLatch的应用场景?

    CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。 CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

    CountDownLatch的使用场景:

    在一些应用场合中,需要等待某个条件达到要求后才能做后面的事情;同时当线程都完成后也会触发事件,以便进行后面的操作, 这个时候就可以使用CountDownLatch。

    CyclicBarrier 使用场景

    CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。

    12.CyclicBarrier和CountDownLatch的区别

    1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。

    2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

    3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

    4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。

    13.synchronized的作用?

    在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。

    synchronized既可以加在一段代码上,也可以加在方法上。

    14.volatile关键字的作用

    对于可见性,Java提供了volatile关键字来保证可见性。

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。

    15.什么是CAS

    CAS是compare and swap的缩写,即我们所说的比较交换。

    cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

    CAS操作包含三个操作数 ——内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。

    16. CAS的问题

    1)CAS容易造成ABA问题。一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。

    2) 不能保证代码块的原子性

    CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

    3)CAS造成CPU利用率增加。之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

    17.什么是Future?

    在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。

    Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

    18.什么是AQS

    AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态

    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

    AQS支持两种同步方式:

    1.独占式

    2.共享式

    这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

    19.ReadWriteLock是什么

    首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

    因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    20.FutureTask是什么

    这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    21.synchronized和ReentrantLock的区别

    synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

    (1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

    (2)ReentrantLock可以获取各种锁的信息

    (3)ReentrantLock可以灵活地实现多路通知

    另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

    22.什么是乐观锁和悲观锁

    (1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

    (2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

    23.线程B怎么知道线程A修改了变量

    • volatile修饰变量
    • synchronized修饰修改变量的方法
    • wait/notify
    • while轮询

    24.synchronized、volatile、CAS比较

    synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。volatile提供多线程共享变量可见性和禁止指令重排序优化。CAS是基于冲突检测的乐观锁(非阻塞)

    25.sleep方法和wait方法有什么区别?

    这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

    26.ThreadLocal是什么?有什么用?

    ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

    简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

    27.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

    这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

    28.多线程同步有哪几种方法?

    Synchronized关键字,Lock锁实现,分布式锁等。

    29.线程的调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

    (1)线程体中调用了yield方法让出了对cpu的占用权利

    (2)线程体中调用了sleep方法使线程进入睡眠状态

    (3)线程由于IO操作受到阻塞

    (4)另外一个更高优先级线程出现

    (5)在支持时间片的系统中,该线程的时间片用完

    30.ConcurrentHashMap的并发度是什么

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

    31.Java死锁以及如何避免?

    Java中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java死锁情况出现至少两个线程和两个或更多资源

    Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

    死锁的原因

    1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

    例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

    2)默认的锁申请操作是阻塞的

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。 总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

    32.怎么唤醒一个阻塞的线程

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

    33.不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    34.什么是多线程的上下文切换

    多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

    35.如果你提交任务时,线程池队列已满,这时会发生什么

    这里区分一下:

    • 如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
    • 如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

    36.Java中用到的线程调度算法是什么

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

    37.什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

    线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

    38.Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    • 可以使锁更公平
    • 可以使线程在等待锁的时候响应中断
    • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    • 可以在不同的范围,以不同的顺序获取和释放锁

    39.单例模式的线程安全性

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

    (1)饿汉式单例模式的写法:线程安全

    (2)懒汉式单例模式的写法:非线程安全

    (3)双检锁单例模式的写法:线程安全

    40.Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    41.Executors类是什么?

    Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。

    Executors可以用于方便的创建线程池

    42.线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

    (1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

    (2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

    43.同步方法和同步块,哪个是更好的选择

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好

    44.Java线程数过多会造成什么异常?

    1)线程的生命周期开销非常高

    2)消耗过多的CPU资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。

    3)降低稳定性

    JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。

    展开全文
  • 1、并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他...synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,释放之前最新的值刷新到主内...

     

    1、并发编程三要素?

     

    1)原子性

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

    2)可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

    实现可见性的方法:

    synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

    3)有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行。

     

    2、多线程的价值?

     

    1)发挥多核CPU的优势

    多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

     

    2)防止阻塞

    从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

     

    3)便于建模

    这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

     

    3、创建线程的有哪些方式?

    1)继承Thread类创建线程类

    2)通过Runnable接口创建线程类

    3)通过Callable和Future创建线程

     

    4.创建线程的三种方式的对比?

    1)采用实现Runnable、Callable接口的方式创建多线程。

    优势是

    线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势是:

    编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

    2)使用继承Thread类的方式创建多线程

    优势是:

    编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    劣势是:

    线程类已经继承了Thread类,所以不能再继承其他父类。

    3)Runnable和Callable的区别

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

     

    5、线程的状态流转图

    线程的生命周期及五种基本状态:

     

    Java线程具有五中基本状态

    1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

    2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    2.同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    3.其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

     

    6.什么是线程池? 有哪几种创建方式?

    线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

    java 提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

     

    四种线程池的创建

    (1)newCachedThreadPool创建一个可缓存线程池

    (2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

    (3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    (4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

     

    7.线程池的优点?

    1)重用存在的线程,减少对象创建销毁的开销。

    2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

    3)提供定时执行、定期执行、单线程、并发数控制等功能。

     

    8.Java中的同步集合与并发集合有什么区别?

    同步集合类:

    •  Vector
    •  Stack
    •  HashTable
    •  Collections.synchronized方法生成

    并发集合类:

    •  ConcurrentHashMap
    •  CopyOnWriteArrayList
    •  CopyOnWriteArraySet等

     

    9.同步集合与并发集合的区别

    同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。同步集合比并发集合会慢得多,主要原因是锁,同步集合会对整个May或List加锁,而并发集合例如ConcurrentHashMap,
    把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段(JDK1.8版本底层加入了红黑树)。

     

    10.常用的并发工具类有哪些?

    •  CountDownLatch
    •  CyclicBarrier
    •  Semaphore
    •  Exchanger

     

    11.CyclicBarrier和CountDownLatch的应用场景?

    CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。 CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

    CountDownLatch的使用场景:

    在一些应用场合中,需要等待某个条件达到要求后才能做后面的事情;同时当线程都完成后也会触发事件,以便进行后面的操作, 这个时候就可以使用CountDownLatch。

     

    CyclicBarrier 使用场景

    CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。

     

    12.CyclicBarrier和CountDownLatch的区别

    1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。

    2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

    3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

    4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。

     

    13.synchronized的作用?

    在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。

    synchronized既可以加在一段代码上,也可以加在方法上。

     

    14.volatile关键字的作用

    对于可见性,Java提供了volatile关键字来保证可见性。

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。

     

    15.什么是CAS

    CAS是compare and swap的缩写,即我们所说的比较交换。

    cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

    CAS
    操作包含三个操作数 ——
    内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。

     

    16. CAS的问题

    1)CAS容易造成ABA问题。一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。

    2) 不能保证代码块的原子性

    CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

    3)CAS造成CPU利用率增加。之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

     

    17.什么是Future?

    在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。

    Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

     

    18.什么是AQS

    AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

     

    AQS支持两种同步方式:

    1.独占式

    2.共享式

    这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

     

    19.ReadWriteLock是什么

    首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

    因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

     

    20.FutureTask是什么

    这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

     

    21.synchronized和ReentrantLock的区别

    synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

    (1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

    (2)ReentrantLock可以获取各种锁的信息

    (3)ReentrantLock可以灵活地实现多路通知

    另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

     

    22.什么是乐观锁和悲观锁

    (1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

    (2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

     

    23.线程B怎么知道线程A修改了变量

    •  volatile修饰变量
    •  synchronized修饰修改变量的方法
    •  wait/notify
    •  while轮询

    24.synchronized、volatile、CAS比较

    •  synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
    •  volatile提供多线程共享变量可见性和禁止指令重排序优化。
    •  CAS是基于冲突检测的乐观锁(非阻塞)

    25.sleep方法和wait方法有什么区别?

    这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

     

    26.ThreadLocal是什么?有什么用?

    ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

    简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

     

    27.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

    这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

     

    28.多线程同步有哪几种方法?

    Synchronized关键字,Lock锁实现,分布式锁等。

     

    29.线程的调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

    (1)线程体中调用了yield方法让出了对cpu的占用权利

    (2)线程体中调用了sleep方法使线程进入睡眠状态

    (3)线程由于IO操作受到阻塞

    (4)另外一个更高优先级线程出现

    (5)在支持时间片的系统中,该线程的时间片用完

     

    30.ConcurrentHashMap的并发度是什么

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

     

    31.Java死锁以及如何避免?

    Java中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java死锁情况出现至少两个线程和两个或更多资源。

    Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

    死锁的原因

    1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

    例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

    2)默认的锁申请操作是阻塞的

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。 总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

     

    32.怎么唤醒一个阻塞的线程

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

     

    33.不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

     

    34.什么是多线程的上下文切换

    多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

     

    35.如果你提交任务时,线程池队列已满,这时会发生什么

    这里区分一下:

    1.  如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
    2.  如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

     

    36.Java中用到的线程调度算法是什么

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

     

    37.什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

    线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

     

    38.Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    •  可以使锁更公平
    •  可以使线程在等待锁的时候响应中断
    •  可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    •  可以在不同的范围,以不同的顺序获取和释放锁

    39.单例模式的线程安全性

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

    (1)饿汉式单例模式的写法:线程安全

    (2)懒汉式单例模式的写法:非线程安全

    (3)双检锁单例模式的写法:线程安全

     

    40.Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

     

    41.Executors类是什么?

    Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。

    Executors可以用于方便的创建线程池

     

    42.线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

    (1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

    (2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

     

    43.同步方法和同步块,哪个是更好的选择

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好

     

    44.Java线程数过多会造成什么异常?

    1)线程的生命周期开销非常高

    2)消耗过多的CPU资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。

    3)降低稳定性

    JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。

     

    展开全文
  • synchronized 拿到的都是对象,而不是把一段代码、方法的多个线程就次有该方法的对象。2个对象,线程获取就是2个对象不同的(互不影响)。 3.有种情况就是相同的,就是在该synchronized 方法是用...

    多个线程多个锁

        1.多个线程,每个线程都可以去拿到自己制定的锁(object) ,分别获取锁后,执行 synchronized 方法。

        2. synchronized 拿到的锁都是对象锁,而不是把一段代码、方法的锁,多个线程就次有该方法的对象锁。2个对象,线程获取就是2个对象不同的锁(互不影响)。

        3.有一种情况就是相同的锁,就是在该 synchronized 方法是用static关键字,表示锁定 class 类,类级别的锁独占l class 类。


        

    1. /**
    2. * Created by liudan on 2017/5/29.
    3. */
    4. public class MyThread2 extends Thread {
    5. private static int num = 0;
    6. public static synchronized void printNum(String str) {
    7. try {
    8. if (str.equals("a")) {
    9. num = 1000;
    10. System.err.println("thread -> A over");
    11. Thread.sleep(1000);
    12. } else if (str.equals("b")) {
    13. num = 200;
    14. System.err.println("thread -> B over");
    15. }
    16. System.err.println("str:" + str + "\tnum:" + num);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. }
    21. public static void main(String[] args) {
    22. //2个不同的对象,只能new一次
    23. final MyThread2 n1 = new MyThread2();
    24. final MyThread2 n2 = new MyThread2();
    25. Thread t1 = new Thread(new Runnable() {
    26. @Override
    27. public void run() {
    28. n1.printNum("a");
    29. }
    30. });
    31. Thread t2 = new Thread(new Runnable() {
    32. @Override
    33. public void run() {
    34. n2.printNum("b");
    35. }
    36. });
    37. t1.start();
    38. t2.start();
    39. }
    40. }
    41. 输出

    thread -> B over str:b num:200 thread -> A over str:a num:1000

     

    转载于:https://www.cnblogs.com/xxt19970908/p/7337133.html

    展开全文
  • 并发:同一个对象被多个线程同时操作,也就是不同线程同时操作同一个资源地址,造成数据紊乱。 同步:多个需要同时访问资源的线程进入对象的等待池,等待前面线程使用完毕。 锁:每个对象都有把锁,当获取对象时,...

    前言

    前面的文章介绍了并发的情况下会有数据错误的现象出现。

    • 并发:同一个对象被多个线程同时操作,也就是不同线程同时操作同一个资源地址,造成数据紊乱。
    • 同步:多个需要同时访问资源的线程进入对象的等待池,等待前面线程使用完毕。
    • 锁:每个对象都有把锁,当获取对象时,独占资源,其他线程必须等待,使用结束后才释放
      • 一个线程持有锁会导致其他所有需要此锁的线程挂起
      • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
      • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

    一、同步方法

    首先得介绍一下 synchronized 关键字。

    public synchronized void method(int args){}
    
    • synchronized:

      • 默认锁的是他自身的对象。
      • 要是跨对象,通常使用同步块。
      • 即锁共享资源所在的对象。
    • 代码示例:

    /**
     * 手机抢购案例
     */
    public class PhoneSnapUp implements Runnable {
    
        private Integer inventory = 10;//手机库存
        private boolean flag = true;
    
        public static void main(String[] args) {
            PhoneSnapUp phoneSnapUp = new PhoneSnapUp();
            //模拟5人同时抢购,即同时开启5个线程
            new Thread(phoneSnapUp, "丁大大1号").start();
            new Thread(phoneSnapUp, "丁大大2号").start();
            new Thread(phoneSnapUp, "丁大大3号").start();
            new Thread(phoneSnapUp, "丁大大4号").start();
            new Thread(phoneSnapUp, "丁大大5号").start();
        }
    
        @Override
        public synchronized void run() {
            while (flag){
                try {
    //                synchronized (this) {
                        buy();
    //                }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        //购票方法提出来
        private void buy() throws Exception {
            //当库存为0时,抢购结束
            if (inventory <= 0) {
                flag = false;
                return;
            }
            //模拟延迟,否则结果不容易看出来
            Thread.sleep(500);
            //每次抢购完,库存减1
            System.out.println("恭喜!!" + Thread.currentThread().getName() + "--抢到了一台小米12!库存还剩:" + --inventory + "台");
        }
    }
    
    
    • 执行结果如下:在这里插入图片描述
    • 总结:同步方法可以做到保证结果不会出错。
      • 但是因为是方法,一加上synchronized 整个方法内的所有内容就相当于被加上了锁,可能会引起效率问题,导致程序阻塞~
      • 所以一般我们都是使用同步代码块来实现加锁的操作。

    二、同步代码块

    synchronized(Obj){}
    
    • 代码案例:其他代码和同步方法一致,不在重复粘贴,这里只介绍run方法里的代码。
    @Override
        public void run() {
            while (flag){
                try {
                    synchronized (this) {
                        buy();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
    • 执行结果:
      在这里插入图片描述

    • 总结:

      • 结果和同步方法是一致的,证明两种方法都可以保证结果的准确性。
      • 同步代码块是指定锁住固定的东西,在方法里其他的内容不受影响。
      • 举个粗暴的例子,你拉屎去,同步方法相当于直接把厕所大门锁住,三个坑位你只使用了一个,典型的浪费资源。而同步代码块,你上了哪个坑位就锁住坑位的门,其他的人可以正常使用其他坑位,哈哈 形象~
    • 注意点:

      • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
      • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身或者是class
    • 执行过程:同步监视器的执行过程:

      • 第一个线程访问,锁定同步监视器,执行其中代码
      • 第二个线程访问 ,发现同步监视器被锁定,无法访问
      • 第一个线程访问完毕 ,解锁同步监视器.
      • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

    三、死锁

    上面一直在讲 ,那么如果两个线程同时拿到某一个共享资源时,都无法释放时,就会造成死锁现象

    • 简单粗暴的理解:

      • 两个资源 A和B
      • 丁一 拿着A资源的锁,执行完想继续拿资源B的锁
      • 丁二 拿着B资源的锁,执行完想继续拿资源A的锁
      • 两个人都没有执行完,也就是互相没有释放锁,两个人就会打起来,谁都执行不了
      • 导致死锁
    • 文字总是理解不清晰,跟着代码去走一圈肯定可以理解的:

    /**
     * 模拟多线程死锁的情况
     */
    public class Demo {
        public static void main(String[] args) {
            DeadLockThread deadLockThread = new DeadLockThread();
            new Thread(deadLockThread,"丁一+").start();
            new Thread(deadLockThread,"丁二+").start();
        }
    }
    
    class DeadLockThread implements Runnable {
        A a = new A();
        B b = new B();
    
        @SneakyThrows
        @Override
        public void run() {
            if (Thread.currentThread().getName().equals("丁一")){
                synchronized (this){
                    System.out.println(Thread.currentThread().getName());
                    a.soutA();
                    Thread.sleep(2000);
                    b.soutB();
                }
            }else {
                synchronized (this) {
                    System.out.println(Thread.currentThread().getName());
                    b.soutB();
                    Thread.sleep(2000);
                    a.soutA();
                }
            }
        }
    }
    
    
    class A {
        public void soutA(){
            System.out.println("我是A");
        }
    }
    
    class B {
        public void soutB(){
            System.out.println("我是B");
        }
    }
    
    
    • 执行结果如下:
      在这里插入图片描述

    • 总结:产生死锁的四个必要条件:

      • 互斥条件: 一个资源每次只能被一个进程使用
      • 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放
      • 不剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺
      • 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
    • 只要破除任意一个就能避免死锁

    四、Lock锁

    • 通过显示定义同步锁对象(Lock)来实现同步

    • ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

    • 代码案例:

    /**
     * 使用Lock锁
     */
    public class TestLock {
    
        public static void main(String[] args) {
            TestLock2 testLock = new TestLock2();
    
            new Thread(testLock, "A").start();
            new Thread(testLock, "B").start();
            new Thread(testLock, "C").start();
        }
    
    }
    
    class TestLock2 implements Runnable{
        int count = 1000;
    
        //定义lock锁
        private final ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            while(true)
            {
                try {
                    //进入加锁状态
                    lock.lock();
                    if(count > 0)
                        System.out.println(Thread.currentThread().getName() + "---" +count--);
                    else
                        break;
                }
                finally {
                    //解锁
                    lock.unlock();
                }
            }
        }
    }
    
    • 执行结果:
      在这里插入图片描述
    • 总结:
      • 在不加锁的情况下,ABC可能会同时操作到count,导致数据紊乱
      • 在加了锁之后,ABC排队操作

    路漫漫其修远兮,吾必将上下求索~
    如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧**转载请注明出处哦**

    Java多线程扩展:Java多线程学习汇总

    展开全文
  • 1、并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在...synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,释放之前最新的值刷新到主内存,实现可见性。 3)有...
  • 可以给共享资源加一把锁,哪个线程获取了这把锁,才有权利访问该共享资源。 1.同步监视器 同步监视器可以是任何对象,必须唯一,保证多个线程获得是同一个对象(锁). 同步监视器的执行过程 1.第一个线程访问,锁定同步...
  • 线程的生命周期 死锁 ...如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻
  • 1、并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程...synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,释放之前最新的值刷新到主内存,实现可见...
  • 线程锁+线程池+线程同步等

    千次阅读 2019-04-28 16:20:39
    1、并发编程三要素? 1)原子性:原子性指的是一个或者多个操作,要么全部执行并且在执行的...synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,释放之前最新的值刷新到主内存,实现可见性。...
  • posix多线程有感--自旋

    千次阅读 2013-05-09 19:54:18
     当线程A想要获取一把自旋而该又被其它线程锁持有时,线程A会在一循环中自旋以检测是不是已经可用了。对于自选需要注意: 由于自旋时不释放CPU,因而持有自旋线程应该尽快释放自旋,否则等待该...
  • 所有的非静态同步方法用的都是同一把锁——实例对象本身,也就是说如果一实 例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获 取锁的方法释放锁后才能获取锁,可是别的实例对象的非...
  • JAVA内置锁:java内置锁的两种体现就是对象锁和类锁,java内置锁是一互斥锁,同时只能被一... 对象锁:对象锁是用于对象实例方法,或者一对象实例上的,每对象实例只有一把锁,且各个对象实例的锁互不干扰。  
  • 读写锁和互斥锁都可以解决多线程在访问共享资源竞争的问题,不同的是读写锁允许多个线程同时读某个共享变量,在同一时刻只允许个线程对变量进行写操作,因此读写锁的效率要高于互斥锁。 读写锁是几把锁 读写锁...
  • Java多线程-线程

    千次阅读 2017-07-06 20:30:55
    所有的非静态同步方法用的都是同一把锁,即实例对象本身,或者说this对象,如果一实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁。如果别的对象的非...
  • 1、并发编程三要素?1)原子性原子性指的是一个或者...实现可见性的方法:synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,释放之前最新的值刷新到主内存,实现可见性。3)有序性有序性,即...
  • 多线程——5自定义显示

    千次阅读 2020-02-21 09:49:49
    同步lock方法里面首先判断当前锁是否被使用,如果被使用了自己加入一个等待队列(自定义一个集合)然后让自己wait, 如果没有被使用那么先把自己从等待队列移除, 可以直接把锁获取到设置bool值为true。 解锁方法:...
  • 线程安全和

    2021-05-29 20:47:44
    能够获取到锁,多个线程同时抢同一把锁,谁(线程)能够获取到锁, 谁就可以执行到该代码,如果没有获取锁成功 中间需要经历锁的升级过程 如果一致没有获取到锁则会一直阻塞等待。 如果线程A获取锁成功 但
  • linux多线程:自旋

    2020-03-13 15:28:33
    线程A想要获取一把自旋而该又被其它线程锁持有时,线程A会在一循环中自旋以检测是不是已经可用了。 对于使用自选需要注意: 由于自旋时不释放CPU,因而持有自旋线程应该尽快释放自旋,否则等待...
  • 锁的分类不互斥,多个类型可以...3、同一线程能否重复获取同一把锁: 可以——可重入锁 不可——不可重入锁 4、多线程是否可以共享一把锁: 可以——共享锁 不可——独占锁 5、多线程竞争是否排队: 排队——...
  • 特点:多个线程在等待同一把锁,等待时间最长的将获得锁 优点:所有的线程都能获得资源,不会饿死在队列中 缺点:吞吐量下降,除了队列中的第个线程,其余线程都会被阻塞,cpu唤醒线程的开销较大 非公平锁: ...
  • 多线程如何获取monitor

    2017-04-20 13:55:49
    可是我的线程C会去一个线程A唤醒,那么好,当第一个线程唤醒后,会执行synchronized (obj_1) {}的,这样不是会让第一个线程再次获得obj_1的执行权吗?但是现在却阻塞在obj_1.wait()这里不再进行下去了?我就想...
  • 1.多线程同步

    2017-08-10 02:08:00
    实现多线程1.基础Thread类2.实现Runnable接口描述: 1.当多个线程访问MyThread 这个类方法的时候,排队的方式去进行访问处理... 如果拿不到锁,则会一直不断的尝试去获取把锁,直到拿到锁为止,多个线程去尝试去...
  • 只有一把,“读”、“写”是加锁的两种方式。“读”是读模式下加锁状态,“写”是写模式下加锁状态。 一个读写同时只能有一个写者,可以有多个读者,但读者和写者不能同时访问(读共享,写独占)。 写...
  • Java多线程的应用

    2018-06-09 18:53:29
    其实Java多线程中,只是一个很抽象的概念。是为了实现互斥和同步而设的。“”打比方,获取锁可认为是“获取做某个事情的权限”,而“释放”可以认为是做某件事情的权限交给别人了。也可以这样认为,“...
  • 详解c# 线程同步

    2020-12-17 03:49:56
    因为我们要用额外的代码把多个线程同时访问的数据包围起来,并获取和释放个线程同步,如果我们在个代码块忘记获取锁,就有可能造成数据损坏。 2. 使用线程同步会影响性能,获取和释放肯定是需要时间的吧...

空空如也

空空如也

1 2 3 4 5 ... 17
收藏数 330
精华内容 132
关键字:

多个线程获取同一把锁