JAVA多线程的一些注意点

时间:2022-09-08 22:56:25

(整理自网上的各种博客,仅供个人学习)

1.多线程的作用:
(1)发挥cpu的优势,多核cpu
(2)防止阻塞,在单核cpu上使用多线程会造成资源的浪费,但是仍然需要使用,以备单个线程阻塞后不影响其他任务的执行
方便建模,将大的任务分解为多个小的任务。

2.创建线程的方式:
一般就是两种:
(1)继承Thread类
(2)实现Runnable接口
至于哪个好,不用说肯定是后者好,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,面向接口编程也是设计模式6大原则的核心。

3.start和run方法的区别:
只有调用start(),才会表现出多线程的特性,让不同线程的run方法里面的代码交替执行。如果只是调用run()方法,那么代码仍然是同步执行的,必须等待一个线程的run方法里的代码全部执行完毕之后,另一个线程才可以执行run()方法里的代码。

4.Runnable接口和Callable接口(这个我还真没见过)
runnnable接口的run方法的返回值是void,它所做的只是纯粹的执行run()方法中的代码。callable接口中的call()方法是有返回值的,是一个泛型,和future,futureTask配合可以用来获取异步执行的结果。(很有用的特性)

5.CylicBarrier和CountDownLatch的区别(其实我连这两个是干啥的都不知道)
两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了

6.Volatile关键字的作用
(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile变量关键字修饰的变量保证了其在多线程之间的可见性,即每次读到volatile变量,一定是最新的数据。
(2)volatile会禁止语义重排序,这导致了一定程度上的降低代码执行效率,但可以避免一些意想不到的问题。

7.线程安全
如果代码能在多线程下执行和单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
线程安全的几个级别:
(1)不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
(4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

8.java中如何获取线程dump文件:
死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
(1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
(2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,

9.线程出现运行时异常:
如果这个异常没有被捕获的话,这个线程就停止执行。另外,如果这个线程持有某个对象的监视器,那么这个对象的监视器会被立即释放。

10.两个线程之间的共享数据
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll,await/signal/sianalAll进行唤起和等待,比如说阻塞队列BlockingQueue就是为了线程之间共享数据而设计的。

11.sleep和wait方法的区别
sleep和wait都可以用来使线程放弃cpu一定时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait会放弃这个对象的监视器。

12.生产者消费模型的作用
(1)通过平衡生产者的产生能力和消费者的消费能力来提升整个系统的运行效率。
(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要受到相互的制约。

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

14.为什么wait()方法和notify/notifyAll()方法要在同步块中被调用
JDK强制要求。wait()方法和notify/notifyAll()方法在调用前都必须获得对象的锁

15.wait()方法和notify/notifyAll()方法在放弃对象监视器的时候有什么区别
wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

16.为什么要使用线程池
避免频繁地创建和销毁线程,达到线程对象的重用,另外,使用线程池还可以根据项目灵活的控制并发的数目

17.怎么检测一个线程是否持有对象监视器
有方法可以判断某个线程是否持有对象监视器:Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着“某条线程”指的是当前线程。

18.synchronized和ReentrantLock的区别:
synchronized是关键字,ReentranLock是类
ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
ReentranLock底层调用的是Unsafe的park方法枷锁,synchronized的操作时对象头中的mark word。

19.JAVA内存模型
java内存模型将内存分为了主内存和工作内存
简单总结一下Java内存模型的几部分内容:
(1)Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去
(2)定义了几个原子操作,用于操作主内存和工作内存中的变量
(3)定义了volatile变量的使用规则
(4)happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的

20.单例模式的线程安全性
老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:非线程安全
(3)双检锁单例模式的写法:线程安全

21.线程的构造方法和静态块是被哪个线程调用
线程类的构造方法,静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

同步的范围越小越好
锁粗化,为了减少加锁->解锁的次数,有效的额提高代码的执行效率

22.线程池在不同情况下的使用:
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

原文:http://www.codeceo.com/article/40-java-thread-problems.html