【多线程和并发】的相关问题

实现线程之间的通信

当线程间是可以共享资源时,线程间的通信是协调它们的重要的手段。

1)Object类中wait() \ notify() \ notifyAll()方法。

2)用Condition接口。

Condition是被绑定到Lock上的,要创建一个Lock的Condition对象必须用newCondition()方法。在一个Lock对象里面可以创建多个Condition对象,线程可以注册在指定的Condition对象中,从而可以有选择性地进行线程通知,在线程调度上更加灵活。

在Condition中,用await()替换wait(),用signal()替换notify(),用signal()替换notifyAll(),传统线程的通信方式,Condition都可以实现。调用Condition对象中的方法时,需要被包含在lock()和unlock()之间。

3)管道实现线程间的通信。

实现方式:一个线程发送数据到输出管道流,另一个线程从输入管道流中读取数据。

基本流程:

一、创建管道输出流Pipe的OutputStream pos和管道输入流PipedInputStream pis。

二、将pos和pis匹配,pos.connect(pis)。

三、将pos赋给信息输入信息的线程,pis赋给获取信息的线程,就可以实现线程间的通讯了。

Consumer1:0

Consumer1:1

Consumer1:2

Consumer1:3

Consumer2:4

Consumer2:5

Consumer1:6

缺点:

1)管道流只能在两个线程之间传递数据。

线程consumer1和consumer2同时从pis中read数据,当线程producer往管道流中写入一段数据(1,2,3,4,5,6)后,每一个时刻只有一个线程能获取到该数据,并不是两个线程都能获取到producer发送来的数据,因此一个管道流只能用于两个线程间的通讯。

2)管道流只能实现单向发送,如果要两个线程之间互通讯,则需要两个管道流。

线程producer通过管道流向线程consumer发送数据,如果线程consumer想给线程producer发送数据,则需要新建另一个管道流pos1和pis1,将pos1赋给consumer1,将pis1赋给producer1.

四、使用volatile关键字

 

如何确保线程安全?

如果多个线程同时运行某段代码,如果每次运行结果和单线程运行的结果是一样的,而且其他变量的值也和预期的是一样的,就是线程安全的。

Synchronized,Lock,原子类(如AtomicInteger等),同步容器、并发容器、阻塞队列、同步辅助类(比如CountDownLatch,Semaphore,CyclicBarrier)。

 

多线程的优点和缺点

优点:

1)充分利用cpu,避免cpu空转

2)程序响应更快

缺点:

1)上下文切换的开销

当CPU从执行一个线程切换到另一个线程的时候,他需要先存储当前线程的本地数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为"上下文切换"。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。上下文切换并不廉价,如果没有必要,应该减少上下文切换的发生。

2)增加资源消耗

线程在运行的时候需要从计算机里面得到一些资源。除了CPU,线程还需要一些内存来维持它本地的堆栈。它也需要占用操作系统的一些资源来管理线程。

3)编程更复杂

在多线程访问共享数据的时候,要考虑线程安全问题。

写出3条遵循的多线程的最佳实践

1)给线程起个有意义的名字

2)避免锁定和缩小同步的范围

相对于同步方法,我更喜欢同步块,因为它可以拥有对锁的绝对控制权。

3)多用同步辅助类,少用wait和notify

首先,COuntDownLatch,Semaphore、CyclicBarrier这些 同步辅助类简化了编码操作,而且wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和改善,使用这些更高等级的同步工具可以让程序很轻轻松松的获得优化

4)多用并发容器,少用同步容器

ConcurrentHashMap

多线程的性能一定优于单线程吗?

不一定,要看具体的任务以及计算机的配置。比如:

对于单核CPU,如果是CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,因为解压文件需要一直还在那用CPU资源,如果采用多线程,线程切换导致的开销反而会让性能下降。如果是交互类型的任务,肯定是需要使用多线程的。

对于多核CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线程能够更加充分利用每个核的资源。

wait()和sleep()的区别

1)这两个方法来自不同的类,sleep来自Thread类,是静态方法,wait()是Object类里面的方法,和notify()或者notifyAll()方法配套使用,来实现线程间的通信。

2)最主要是sleep是将当前线程挂起指定的时间,没有释放锁;而wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法。

3)使用范围:wait、notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以再任何地方使用。

synchronized(x) {
    x.notify()
    //或者wait()
}


sleep和wait必须捕获异常(Thread.sleep()和Object.wait()都会抛出InterruptedException),notify和notifyAll不需要捕获异常。

Java中interrupted()和isInterrupted()方法的区别

两个方法都是判断线程是否停止的方法。

1)前者是静态方法,后者是非静态方法。interrupted是作用于当前正在运行的线程,isInterrupted是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中调用B线程对象的isInterrupted方法,此时当前正在运行的线程就是A线程。)

2)前者会将中断状态清除而后者不会。

Java创建线程之后,直接调用start()方法和run()方法的区别?

1)start()方法来启动线程,并在新线程中运行run()方法,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程,这时线程是处于就绪状态,并没有运行,然后通过此Thread类调用run()方法来完成其运行操作,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run()方法运行结束,此线程终止。然后CPU再调度其他线程。

2)直接调用run()方法的话,会把run()方法当做普通方法来调用,会在当前线程中执行run()方法,而不会启动新线程来运行run()方法。程序还是要顺序执行,要等待run()方法体执行完毕后,才可以继续执行下面的代码;程序中只有主线程这一个线程,某程序执行路径还是只有一条,这样就没有达到多线程的目的。

什么是线程的上下文切换?

对于单核CPU,CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。

线程上下文切换过程中会记录程序计数器、CPU寄存器的状态等数据。

虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。

怎么检测一个线程是否拥有锁?

在java.lang.Thread中有一个方法叫holdsLock(Object obj),它返回true,如果当且仅当当前线程拥有某个具体对象的锁

用户线程和守护线程有什么区别?

在Java中创建一个线程,他就被称为用户线程。将一个用户线程设置为守护线程的方法就是在调用start()方法之前,调用对象的setDamon(true)方法。一个守护线程是在后台执行并且不会阻止JVM终止的线程,守护线程的作用是为其他线程的运行提供便利服务。当没有用户线程在运行的时候,JVM关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。

守护线程的一个典型例子就是垃圾回收器。

线程的状态


有三个线程T1、T2、T3,怎么保证他们按顺序执行

使用join()方法

在一个主线程中,要求有大量子线程执行完之后,主线程菜执行完成。多种方式,考虑效率。

1)在主函数中使用join()方法。

2)CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

3)使用线程池

Java程序如何停止一个线程

建议使用异常法来终止线程的继续运行。在想要被中断执行的过程中,调用interrupted()方法,该方法用来校验当前线程是否已经被中断,即该线程是否被打上了中断的标记,并不会使得线程立即停止运行,如果返回true,则抛出异常,停止线程的运行。在线程外,调用interrupted()方法,使得该线程打上中断的标记。

如何在两个线程间共享数据

一、每个线程执行的代码相同

若每个线程执行的代码相同,共享数据就比较简单,可以使用同一个Runnable对象,这个Runnable对象就有那个共享数据。

二、每个线程执行的代码不同

如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,将需要共享的数据封装成一个对象,将该对象传给执行不同代码的Runnable对象。

Java中堆和栈有什么不同

栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其他线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中农一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile变量就可以发挥作用了,它要求线程从主存中读取变量值。

Java中的同步容器类和缺陷

在Java中,同步容器类主要包括2类:

1)Vector、HashTable。

2)Collections类中提供的静态工厂方法创建的类。Collections.synchronizedXXX()。

缺陷:

1)性能问题。

在有多个线程进行访问时,如果多个线程都只是进行读取操作,那么每个时刻就只能有一个线程进行读取,其他线程便只能等待,这些线程必须竞争同一把锁。

2)ConcurrentModificationException异常。

在对Vector等容器进行迭代修改时,会报ConcurrentModificationException异常。但是在并发容器中(如ConcurrentHashMap,CopyOnWriteArrayList等)不会出现这个问题。

为什么说ConcurrentHashMap是弱一致性的?以及为何多个线程并发修改ConcurrentHashMap时不会报ConcurrentModificationException?

1)ConcurrentHashMap#get()

正是因为get操作几乎所有时候都是一个无锁操作,使得同一个Segment实例上的put和get可以同时进行,这就是get操作是弱一致性的根本原因。

2)ConcurrentHashMap#clear()

clear方法很简单,代码如下:

public void clear() {
    for (int i = 0;i < segments.length; i++)
        segments[i].clear();
}

因为没有全局的锁,在清除完一个segment之后,正在清理下一个segment的时候,已经清理的segment可能又被加入了新的数据,因此clear返回的时候,ConcurrentHashMap中是可能存在数据的。因此clear方法是弱一致性的。

3)ConcurrentHashMap中的迭代器

在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出 ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致性的表现。

在这种迭代方式中,当iterator被创建后,集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。

总结:ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,折旧与HashTable和同步HashMap一样了。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值