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

动漫推荐 浏览(698)
亚洲通最新官方网址
?我会用多线程很好,有什么用?在我看来,这个答案更加无稽之谈。所谓“知道它知道为什么”,“将使用”只是“知道它”,“为什么使用”是“知道为什么”,只是达到“知道它知道为什么”的水平可以说是知识点可以自由使用。好的,让我谈谈我对这个问题的看法:

(1)利用多核CPU

随着行业的发展,今天的笔记本电脑,台式机甚至商业应用服务器至少是双核的。对于4核,8核甚至16核,这种情况并不少见。如果它是单线程程序,则它位于双核CPU上。在4核CPU上浪费了50%,浪费了75%。单核CPU上的所谓“多线程”是假的多线程。同时,处理器只处理一段逻辑,但线程在它们之间切换得更快,并且“同时”看起来像多个线程。多核CPU上的多线程是真正的多线程。它允许您的多段逻辑同时工作。多线程可以真正利用多核CPU来实现CPU的充分利用。

(2)防止阻止

从程序运行效率的角度来看,单核CPU不仅会发挥多线程的优势,而且会因为在单个线程上运行多个线程而导致的线程上下文切换,从而降低了程序的整体效率。核心CPU。但对于单核CPU,我们仍然需要应用多线程来防止阻塞。想象一下,如果单核CPU使用单个线程,只要线程被阻塞,例如,远程读取某个数据,对等体没有返回并且没有设置超时,那么整个程序将在数据之前返回退回。停止运行多线程可以防止这个问题,多个线程同时运行,即使一个线程的代码执行读取数据阻塞,也不会影响其他任务的执行。

(3)易于建模

这是另一个不那么明显的优点。假设一个大的任务A,单线程编程,那么有很多需要考虑,并且构建整个程序模型很麻烦。但是,如果将这个大任务A分解为几个小任务,分别是任务B,任务C和任务D,建立一个程序模型,并通过多个线程运行这些任务,那就简单多了。

一个更常见的问题,一般是两个:

(1)继承Thread类

(2)实现Runnable接口

至于哪个更好,不言而喻,后者更好,因为实现接口的方式比继承类的方式更灵活,并且它还可以降低程序之间的耦合程度。面向接口的编程也是设计模式的六个原则的核心。

只有对start()方法的调用才会显示多线程功能,并且不同线程的run()方法中的代码将交替执行。如果只调用run()方法,则代码将同步执行。您必须等待一个线程的run()方法中的代码执行,另一个线程可以在其run()方法中执行代码。

有点深层次的问题,但也看到了Java程序员要学习的广度。

Runnable接口中run()方法的返回值为void。它所做的只是简单地在run()方法中执行代码。 Callable接口中的call()方法具有返回值并且是通用的。使用Future,FutureTask可用于获取异步执行的结果。

这实际上是一个非常有用的功能,因为多线程比单线程更难和更复杂的一个重要原因是因为多线程充满了未知数。线程是执行的吗?线程执行了多长时间?是否已经分配了执行线程时我们期望的数据?我不知道,我们所能做的就是等待这个多线程任务完成。 Callable + Future/FutureTask可以获得多线程操作的结果。如果等待时间太长并且未获得所需数据,则取消线程的任务非常有用。

两个看似相似的类都在java.util.concurrent下,可用于指示代码在某个点运行。两者之间的区别是:

(1)在CyclicBarrier线程运行到某一点后,线程停止运行,直到所有线程都到达这一点,并重新运行所有线程; CountDownLatch不是,一个线程运行到某一点。之后,只需给出值-1,线程继续运行

(2)CyclicBarrier只能唤起任务,CountDownLatch可以唤起多个任务

(3)CyclicBarrier可以重用,CountDownLatch不能重用,计数值为0,CountDownLatch不能重用

一个非常重要的问题是每个学习和应用多线程的Java程序员都必须掌握它。理解volatile关键字的作用的前提是理解Java内存模型。我不会在这里讨论Java内存模型。请参阅第31点.SOF关键字有两个主要功能:

(1)多线程主要围绕可见性和原子性两个特征展开。使用volatile关键字修改的变量确保其在多个线程之间的可见性,也就是说,每次读取volatile变量时,它必须是最新的。数据

(2)代码的底层执行并不像我们看到的高级语言那样简单 - Java程序。它的执行是Java代码C>字节码C>根据字节码C>执行相应的C/C ++代码C/C ++代码被编译成汇编语言C>与硬件电路交互。实际上,为了获得更好的性能,JVM可能会重新排序指令,并且在多线程下可能会出现一些意想不到的问题。使用volatile将禁用语义重新排序,这当然会降低代码执行的效率。

从实际角度来看,挥发性的重要作用是与CAS结合以确保原子性。有关详细信息,请参阅java.util.concurrent.atomic包下的类,例如AtomicInteger。

这也是一个理论问题。有很多答案。我给一个人一个最好的解释:如果你的代码在多线程下执行并在一个线程下执行,你将总是得到相同的结果。那么你的代码是线程安全的。

关于这个问题值得一提,也就是说,有几个级别的线程安全性:

(1)不可变的

像String,Integer和Long一样,它们都是最终的类型。没有线程可以改变它们的值。如果您更改了一个,则可以直接更改它们。因此,这些不可变对象可以直接在多线程环境中进行,而无需任何同步。使用

(2)绝对线程安全性

无论运行时环境如何,调用者都不需要其他同步措施。这样做通常会产生很多额外费用。 Java是一个线程安全的类。实际上,它们中的大多数都不是线程安全的,但绝对线程安全的类也可以在Java中使用,例如CopyOnWriteArrayList和CopyOnWriteArraySet。

(3)相对线程安全性

相对线程安全是我们通常所说的线程安全的。与Vector类似,添加和删除方法是不会中断的原子操作,但如果有一个遍历Vector的线程,则仅限于此。在Add向量中同时有一个线程,99%的情况会出现ConcurrentModificationException,这是一种失败快速机制。

(4)线程不安全

没有什么可说的,ArrayList,LinkedList,HashMap等都是线程不安全的类

死循环,死锁,阻塞,慢页打开等,线程转储是解决问题的最佳方法。所谓的线程转储也是线程堆栈。获取线程堆栈有两个步骤:

(1)获取线程的pid,也可以使用jps命令在Linux环境中使用ps-ef | grepjava。

(2)打印线程堆栈,也可以使用jstackpid命令在Linux环境下使用kill-3pid

此外,Thread类提供了一个getStackTrace()方法,该方法也可用于获取线程堆栈。这是一个实例方法,因此每次get都是由特定线程的特定运行队列获取时,此方法都绑定到特定的线程实例,

如果未捕获此异常,则线程将停止执行。另一个要点是,如果此线程持有对象的监视器,则对象监视器将立即释放

通过在线程之间共享对象,您可以唤醒并等待wait/notify/notifyAll,await/signal/signalAll,例如,阻塞队列被设计用于在线程之间共享数据。

经常会遇到这个问题,睡眠方法和等待方法都可以用来放弃CPU一段时间,区别在于如果线程持有对象的监视器,睡眠方法就不会放弃监视器对象,wait方法将放弃此对象监视器

这个问题非常理论化,但重要的是:

(1)通过平衡生产者的生产能力和消费者的消费能力来提高整个系统的运行效率,这是生产者消费者模式中最重要的作用

(2)解耦,这是附加到生产者消费者模型的函数。解耦意味着生产者和消费者之间的沟通较少。链接越少,它们就越能独立发展而不会受到相互制约。 >

简而言之,ThreadLocal是一种随时间改变空间的方法。在每个Thread中,维护由开放地址方法实现的ThreadLocal.ThreadLocalMap,并且数据被隔离。数据未共享。当然,没有线程安全问题。/P>

这是JDK必须的。 wait()方法和notify()/notifyAll()方法必须在调用之前首先获取对象的锁。

放弃对象监视器时wait()方法和notify()/notifyAll()方法之间的区别在于wait()方法立即释放对象监视器,而notify()/notifyAll()方法等待要完成的剩余线程代码。对象监视器被丢弃。

避免频繁创建和销毁线程以实现线程对象的重用。此外,使用线程池允许您根据项目灵活地控制并发数。

我还在因特网上看到了一个多线程访问问题,知道有一种方法可以确定线程是否包含一个对象监视器:Thread类提供了一个holdLock(Objectobj)方法,当且仅当对象的监视器obj时是该线程在保持时将返回true。请注意,这是一个静态方法,这意味着“一个线程”指的是当前线程。

Synchronized是与if,else,for相同的关键字,而ReentrantLock是一个类,这是两者之间的本质区别。由于ReentrantLock是一个类,它提供了比synchronized更灵活的功能,可以继承,可以有方法,可以有多种类变量,并且ReentrantLock的可伸缩性超过synchronized可以反映在几个方面:/p>

(1)ReentrantLock可以设置获取锁的等待时间,从而避免死锁

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

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

另外,两者的锁定机制实际上是不同的。底层的ReentrantLock称为Unsafe的park方法锁,同步操作应该是对象头中的标记。我不确定这一点。

ConcurrentHashMap的并发性是段的大小。默认值为16,这意味着最多有16个线程可以同时操作ConcurrentHashMap。这是ConcurrentHashMap对Hashtable的最大优势。在任何情况下,Hashtable可以同时拥有两个线程来获取Hashtable。数据?

首先,目前还不清楚ReentrantLock是不是很好,但ReentrantLock在某些方面有局限性。如果使用ReentrantLock,可能是为了防止线程A写入数据和线程B读取数据导致的数据不一致。但是,如果线程C正在读取数据而线程D也在读取数据,则读取数据不会改变数据,并且没有必要。锁定但仍然锁定,降低了程序的性能。

正因为如此,读写锁ReadWriteLock诞生了。 ReadWriteLock是一个读写锁定接口。 ReentrantReadWriteLock是ReadWriteLock接口的具体实现。它实现了读写分离。读锁是共享的。写锁是独占的。读取和读取不是互斥,读取和写入。写入和读取,写入和写入将是互斥的,提高了读写性能。

这实际上是前面提到的,FutureTask表示异步操作的任务。在FutureTask中,您可以传入Callable的特定实现类,它可以等待异步操作任务的结果,确定它是否已完成,取消任务等等。当然,由于FutureTask也是Runnable接口的实现类,因此FutureTask也可以放在线程池中。

这是一个更实际的问题。我认为这很有道理。你可以这样做:

(1)获取之前提到的项目的pid,jps或ps-ef | grepjava

(2)top-H-ppid,订单无法更改

这将打印出当前项目,每个线程占用CPU时间的百分比。请注意,这是LWP,它是操作系统的本机线程的线程号。我没有在Linux环境中部署Java项目,因此无法演示屏幕截图。如果公司使用Linux环境来部署项目,您可以尝试一下。

使用“top-H-ppid”+“jpspid”可以很容易地找到占用CPU高线程的线程堆栈,从而将CPU定位得很高,通常是因为代码操作不当导致无限循环。

最后,“top-H-ppid”的LWP是十进制的。 “jpspid”的本地线程号是十六进制的。转换后,您可以找到占用CPU的线程的当前线程。叠起来。

我第一次看到这个话题,并认为这是一个非常好的问题。很多人都知道死锁是什么:线程A和线程B等待彼此的锁定导致程序无限循环。当然,这也仅限于此。我不知道如何编写死锁程序。这种情况是白色的。我不知道死锁是什么。我理解一个理论而且我已经完成了。我在实践中遇到了僵局。基本上它是无法看到的。

真的明白什么是死锁,这个问题并不难,只需几个步骤:

(1)两个线程包含两个Object对象:lock1和lock2。这两个锁充当同步代码块的锁;

(2)在线程1的run()方法中,同步代码块首先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,大约50毫秒,然后获取对象锁lock2。这主要是为了防止线程1连续启动lock1和lock2的对象锁定。

(3)线程2的运行(在该方法中,同步代码块首先获取lock2的对象锁,然后获取lock1的对象锁。当然,lock1的对象锁已由线程1的锁保持。线程2肯定在等待线程.1释放lock1对象锁

因此,线程1“休眠”休眠,线程2获取了lock2的对象锁,并且线程1此时尝试获取lock2的对象锁,并被阻止,此时形成死锁。代码不写,占了很大的空间,Java多线程7:本文中的死锁,是上面步骤的代码实现。

如果线程由于调用wait(),sleep()或join()方法而阻塞,则线程可以被中断并且可以通过抛出InterruptedException来唤醒它;如果线程遇到IO块,则无需执行任何操作,因为IO是操作系统。实现时,Java代码无法直接访问操作系统。

如前所述,不可变对象保证了对象的内存可见性,并且不可变对象的读取不需要额外的同步手段,这提高了代码执行的效率。

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

如果你使用LinkedBlockingQueue,这是一个无界的队列,那么无关紧要,继续向阻塞队列添加任务并等待执行,因为LinkedBlockingQueue几乎可以被认为是无限队列,可以无限期地存储任务;如果您正在使用有界队列,请说在ArrayBlockingQueue的情况下,该任务将首先添加到ArrayBlockingQueue。当ArrayBlockingQueue已满时,将使用拒绝策略RejectedExecutionHandler处理完整任务。默认值为AbortPolicy。

抢占。线程用完CPU后,操作系统根据线程优先级,线程饥饿等计算总优先级,并将下一个时间片分配给线程。

这个问题与上述问题有关,我有联系。因为Java使用抢占式线程调度算法,所以可能会发生某个线程经常获得对CPU的控制。为了允许一些具有较低优先级的线程获得CPU控制,可以使用Thread.sleep(0)手动触发操作系统的操作来分配时间片,这也是一种平衡CPU控制的操作。

许多synchronized中的代码只是一些非常简单的代码,执行时间非常快,等待线程锁定可能是一个不太值得的操作,因为线程阻塞涉及用户状态和内核状态切换。由于synchronized中的代码执行速度非常快,因此不要阻止等待锁的线程,而是在同步边界上执行busy循环,这就是旋转。如果你做了几个繁忙的周期,发现你还没有获得锁定然后被阻止,这可能是一个更好的策略。

Java内存模型定义了对Java内存的多线程访问的规范。 Java内存模型应该是完整的。这里不是一句话。我将简要总结一下Java内存模型的一些部分:

(1)Java内存模型将内存分为主内存和工作内存。类的状态,即类之间共享的变量,存储在主存储器中。每次Java线程使用主存储器中的变量时,主存储器中的变量都会被读取一次并生成内存。你自己的工作记忆中有一份副本。运行自己的线程代码时,使用这些变量,操作就是工作内存中的操作。执行线程代码后,最新值将更新为主存储器

(2)定义了几个原子操作来操作主存和工作存储器中的变量

(3)定义使用volatile变量的规则

(4)发生之前,即先前发生的原则,定义了操作A必须首先在操作B中发生的规则。例如,同一线程中控制流前面的代码必须首先执行后面的代码控制流程,并释放锁定。解锁动作必须首先在锁定同一锁等的锁定动作中发生,只要满足规则,就不需要额外的同步措施。如果一段代码不符合所有先发生的规则,则此代码必须是线程不安全的

CAS,全名是CompareandSwap,这是比较替换。假设存在三个操作数:存储器值V,旧期望值A,要修改的值B,并且存储器值被修改为B并且当且仅当期望值A和存储器值V是同样,否则什么都不做,并返回假。当然,CAS必须匹配volatile变量,以便每次获得的变量是主存储器中的最新值,否则旧的期望值A将始终是线程的值A,只要CAS操作失败即可,它永远不会成功。

(1)乐观锁:就其名称而言,它对并发操作引起的线程安全问题持乐观态度。乐观锁相信竞争并不总是发生,因此它不需要持有锁,它将比较 - 替换这两者。作为原子操作,操作会尝试修改内存中的变量。如果失败,则表示发生冲突,则应该有相应的重试逻辑。

(2)悲观锁:仍然喜欢它的名字。对于并发操作引起的线程安全问题,悲观锁认为竞争总是发生,所以每次资源运行时,它都会保持独占锁,就像同步一样,无论三七二十一,都直接在锁定以操作资源。

简单说说AQS,AQS叫做AbstractQueuedSychronizer,翻译应该是一个抽象队列同步器。

如果java.util.concurrent的基础是CAS,则AQS是整个Java并发包的核心,并且使用ReentrantLock,CountDownLatch,Semaphore等。 目。例如,ReentrantLock,所有等待的线程都放在一个Entry中并连接到一个双向队列。前一个线程使用ReentrantLock,双向队列实际上是第一个。 Entry开始运行。

AQS定义双向队列上的所有操作,并且仅为开发人员打开tryLock和tryRelease方法。开发人员可以根据其实现覆盖tryLock和tryRelease方法,以实现自己的并发性。

老式的问题,首先要说的是单例模式的线程安全性意味着类的实例只能在多线程环境中创建一次。有很多方法可以编写单例模式,所以让我总结一下:

(1)饥饿的中国单身人士模式:线程安全

(2)懒惰的中文单例模式:非线程安全

(3)双重检查锁定单例模式的方法:线程安全

信号量是一个信号量,其作用是限制代码块的并发数。信号量有一个构造函数,可以传递一个int整数n,表示某个代码段最多可以访问n个线程。如果它超过n,则等待,等待一个线程完成执行代码块,即下一个线程。再次输入。可以看出,如果在信号量构造函数中传递的int整数是n=1,则它等同于同步。

这在我面前是一个混乱。我不知道你是否考虑过它。如果方法中有多个语句并且所有语句都运行相同的类变量,那么在多线程环境中锁定将不可避免地导致线程安全问题。这很好理解,但size()方法显然只有一个语句。你为什么要锁?

在这个问题上,理解和工作缓慢有两个主要原因。

(1)只有一个线程可以同时执行固定类的同步方法,但对于类的异步方法,多个线程可以同时访问。所以,这有一个问题。线程A可能在Hashtable的put方法中添加数据。线程B可以调用size()方法来读取Hashtable中当前的元素数。读取的值可能不是最新的。线程A可能已经添加了数据,但是没有大小++,线程B已经读取了大小,那么线程B的大小必须是不准确的。向size()方法添加同步后,这意味着线程B调用size()方法,只有在线程A调用put方法后才能调用该方法,从而确保线程安全

(2)CPU执行代码,执行不是Java代码。这非常重要,必须记住。 Java代码最终被转换为汇编代码,这是真正与硬件电路交互的代码。即使您看到Java代码只有一行,即使您看到编译Java代码后生成的字节码只有一行,也不意味着该语句只有一个操作。假设句子“returncount”被翻译成汇编语句执行的三个句子。完全可以执行第一句并且线程切换。

这是一个非常有问题和令人尴尬的问题。请记住,线程类的构造函数和静态块由新线程所在的线程调用,而run方法中的代码由线程本身调用。

如果上面的语句让你感到困惑,那么让我举一个例子,假设Thread1有新的Thread1并且main函数有新的Thread2,那么:

(1)Thread2的构造函数,静态块由主线程调用,而Thread2的run()方法由Thread2本身调用

(2)Thread1的构造函数,静态块由Thread2调用,Thread1的run()方法由Thread1本身调用

同步块,这意味着同步块外部的代码是异步执行的,这比同步整个方法更有效。请知道一个原则:同步越小越好。

有了这个,我想提一下,尽管同步范围尽可能小,但Java虚拟机中仍然存在一种称为锁定粗化的优化方法,即增加同步范围。这很有用,例如StringBuffer,它是一个线程安全的类。当然,最常用的append()方法是同步方法。当我们编写代码时,它会重复追加字符串,这意味着重复锁定。 - >解锁,这对性能不利,因为这意味着Java虚拟机将在此线程上反复切换内核模式和用户模式,因此Java虚拟机将对append方法调用的代码执行锁定粗化。将append操作多次延伸到append方法的开头和结尾的操作变成了一个大的同步块,从而减少了锁C>解锁次数有效地提高了代码执行的效率。

这是我在并发编程网络上看到的一个问题。我把这个问题放在最后一个问题上。我希望每个人都能看到和思考它,因为它非常好,非常实用,非常专业。关于这个问题的个人意见是:

(1)高并发,任务执行时间短,线程池线程数可设置为CPU核心编号+1,减少线程上下文切换

(2)应该区分具有低并发性和长任务执行时间的企业:

a)如果业务时间长期集中在IO操作上,也就是IO密集型任务,因为IO操作不占用CPU,所以不要让所有CPU空闲,可以增加线程数量线程池,让CPU处理更多业务

b)如果计算操作中的业务时间很长,即计算密集型任务,则没有办法,并且(1),线程池中的线程数设置得更少,并且线程上下文切换是降低。/P>

(3)高并发性和长服务执行时间。解决此类任务的关键不是线程池,而是整体架构设计。这是查看这些服务中的某些数据是否可以缓存的第一步。两个步骤,对于线程池设置,设置引用(2)。最后,还可能需要分析长业务执行时间的问题,看看是否可以使用中间件来拆分和解耦任务。

========================================================================================================================================================复制,不粘贴,以确保每一句话,每行代码都经过仔细审查并仔细考虑。在每篇文章的背后,我希望看到我对技术和生活的态度。

我相信乔布斯说只有那些疯狂到足以认为他们能改变世界的人才能真正改变这个世界。面对压力,我可以拿起灯,晚上打架;面对困难,我愿意面对困难,永不退缩。

事实上,我想说的是我只是一名程序员,这就是我现在所拥有的一切。

最后,为了帮助大家更好地学习java,通过访谈更好,小编在这里也为大家准备了最新的BAT大企业面试问题,包括23种设计模式,JVM,性能优化,开源框架等。面试问题

数据采集方法:

请添加JAVA架构技术交流组:

点击链接加入群聊[JAVA高级架构技术交流]:

40阿里巴巴JAVA研发多线程面试问题,你能回答多少?