Java中实现多线程有几种方法
继承
Thread
类;
实现
Runnable
接口;
实现
Callable
接口通过
FutureTask
包装器来创建
Thread
线程;
使用
ExecutorService
、
Callable
、
Future
实现有返回结果的多线程(也就是使用了
ExecutorService
来
管理前面的三种方式)。
如何停止一个正在运行的线程
1
、使用退出标志,使线程正常退出,也就是当
run
方法完成后线程终止。
2
、使用
stop
方法强行终止,但是不推荐这个方法,因为
stop
和
suspend
及
resume
一样都是过期作废的
方法。
3
、使用
interrupt
方法中断线程。
class
MyThread
extends
Thread
{
volatile
boolean
stop
=
false
;
public
void
run
() {
while
(
!
stop
) {
System
.
out
.
println
(
getName
()
+
" is running"
);
try
{
sleep
(
1000
);
}
catch
(
InterruptedException e
) {
System
.
out
.
println
(
"week up from blcok..."
);
stop
=
true
;
//
在异常处理代码中修改共享变量的状态
}
}
System
.
out
.
println
(
getName
()
+
" is exiting..."
);
}
}
class
InterruptThreadDemo3
{
public static
void
main
(
String
[]
args
)
throws
InterruptedException
{
MyThread m1
=
new
MyThread
();
System
.
out
.
println
(
"Starting thread..."
);
m1
.
start
();
Thread
.
sleep
(
3000
);
System
.
out
.
println
(
"Interrupt thread...: "
+
m1
.
getName
());
m1
.
stop
=
true
;
//
设置共享变量为
true
m1
.
interrupt
();
//
阻塞时退出阻塞状态
Thread
.
sleep
(
3000
);
//
主线程休眠
3
秒以便观察线程
m1
的中断情况
System
.
out
.
println
(
"Stopping application..."
);
}
}
notify()
和
notifyAll()
有什么区别?
notify
可能会导致死锁,而
notifyAll
则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行
synchronized
中的代码
使用
notifyall,
可以唤醒
所有处于
wait
状态的线程,使其重新进入锁的争夺队列中,而
notify
只能唤醒一个。
wait()
应配合
while
循环使用,不应使用
if
,务必在
wait()
调用前后都检查条件,如果不满足,必须调用
notify()
唤醒另外的线程来处理,自己继续
wait()
直至条件满足再往下执行。
class
MyThread
extends
Thread
{
volatile
boolean
stop
=
false
;
public
void
run
() {
while
(
!
stop
) {
System
.
out
.
println
(
getName
()
+
" is running"
);
try
{
sleep
(
1000
);
}
catch
(
InterruptedException e
) {
System
.
out
.
println
(
"week up from blcok..."
);
stop
=
true
;
//
在异常处理代码中修改共享变量的状态
}
}
System
.
out
.
println
(
getName
()
+
" is exiting..."
);
}
}
class
InterruptThreadDemo3
{
public static
void
main
(
String
[]
args
)
throws
InterruptedException
{
MyThread m1
=
new
MyThread
();
System
.
out
.
println
(
"Starting thread..."
);
m1
.
start
();
Thread
.
sleep
(
3000
);
System
.
out
.
println
(
"Interrupt thread...: "
+
m1
.
getName
());
m1
.
stop
=
true
;
//
设置共享变量为
true
m1
.
interrupt
();
//
阻塞时退出阻塞状态
Thread
.
sleep
(
3000
);
//
主线程休眠
3
秒以便观察线程
m1
的中断情况
System
.
out
.
println
(
"Stopping application..."
);
}
}
notify()
是对
notifyAll()
的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死
锁。正确的场景应该是
WaitSet
中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果
唤醒的线程无法正确处理,务必确保继续
notify()
下一个线程,并且自身需要重新回到
WaitSet
中
.
sleep()
和
wait()
有什么区别?
对于
sleep()
方法,我们首先要知道该方法是属于
Thread
类中的。而
wait()
方法,则是属于
Object
类中的。
sleep()
方法导致了程序暂停执行指定的时间,让出
cpu
该其他线程,但是他的监控状态依然保持者,当 指定的时间到了又会自动恢复运行状态。在调用sleep()
方法的过程中,线程不会释放对象锁。
当调用
wait()
方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
volatile
是什么
?
可以保证有序性吗
?
一旦一个共享变量(类的成员变量、类的静态成员变量)被
volatile
修饰之后,那么就具备了两层语
义:
1
)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile
关键字会强制将修改的值立即写入主存。
2
)禁止进行指令重排序。
volatile
不是原子性操作
什么叫保证部分有序性
?
当程序执行到
volatile
变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果 已经对后面的操作可见;在其后面的操作肯定还没有进行;
x
=
2
;
//
语句
1
y
=
0
;
//
语句
2
flag
=
true
;
//
语句
3
x
=
4
;
//
语句
4
y
= -
1
;
//
语句
5
由于
flflag
变量为
volatile
变量,那么在进行指令重排序的过程的时候,不会将语句
3
放到语句
1
、语句
2
前 面,也不会讲语句3
放到语句
4
、语句
5
后面。但是要注意语句
1
和语句
2
的顺序、语句
4
和语句
5
的顺序是不作任何保证的。
使用
Volatile
一般用于 状态标记量 和 单例模式的双检锁
Thread
类中的
start()
和
run()
方法有什么区别?
start()
方法被用来启动新创建的线程,而且
start()
内部调用了
run()
方法,这和直接调用
run()
方法的效果不一样。当你调用run()
方法的时候,只会是在原来的线程中调用,没有新的线程启动,
start()
方法才会启动新线程。
为什么
wait, notify
和
notifyAll
这些方法不在
thread
类里面?
明显的原因是
JAVA
提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需
要等待某些锁那么调用对象中的
wait()
方法就有意义了。如果
wait()
方法定义在
Thread
类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait
,
notify
和
notifyAll
都是锁级别的操作,所以把他们定义在Object
类中因为锁属于对象。
为什么
wait
和
notify
方法要在同步块中调用?
1.
只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的
wait(),notify()
和
notifyAll()
方法。
2.
如果你不这么做,你的代码会抛出
IllegalMonitorStateException
异常。
3.
还有一个原因是为了避免
wait
和
notify
之间产生竞态条件。
wait()
方法强制当前线程释放对象锁。这意味着在调用某对象的
wait()
方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()
方法。 在调用对象的notify()
和
notifyAll()
方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()
或
notifyAll()
方法。
调用
wait()
方法的原因通常是,调用线程希望某个特殊的状态
(
或变量
)
被设置之后再继续执行。调用
notify()
或
notifyAll()
方法的原因通常是,调用线程希望告诉其他等待中的线程
:"
特殊状态已经被设置
"
。 这个状态作为线程间通信的通道,它必须是一个可变的共享状态(
或变量
)
。
Java
中
interrupted
和
isInterruptedd
方法的区别?
interrupted()
和
isInterrupted()
的主要区别是前者会将中断状态清除而后者不会。
Java
多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()
来中断一个线程就会设置中断标识为
true
。当中断线程调用静态方法Thread.interrupted()
来检查中断状态时,中断状态会被清零。而非静态方法 isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出
InterruptedException
异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。
Java
中
synchronized
和
ReentrantLock
有什么不同?
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行 线程阻塞和唤醒的代价是比较高的.
区别:
这两种方式最大区别就是对于
Synchronized
来说,它是
java
语言的关键字,是原生语法层面的互斥,需要jvm
实现。而
ReentrantLock
它是
JDK 1.5
之后提供的
API
层面的互斥锁,需要
lock()
和
unlock()
方法配 合try/fifinally
语句块来完成。
Synchronized
进过编译,会在同步块的前后分别形成
monitorenter
和
monitorexit
这个两个字节码指
令。在执行
monitorenter
指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1
,相应的,在执行
monitorexit
指令时会将锁计算器就减
1
,当计算器为0
时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
由于
ReentrantLock
是
java.util.concurrent
包下提供的一套互斥锁,相比
Synchronized
,
ReentrantLock
类提供了一些高级功能,主要有以下
3
项:
1.
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized来说可以避免出现死锁的情况。
2.
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,
Synchronized
锁非公平锁, ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数
true
设为公平锁,但公平锁表现的性 能不是很好。
3.
锁绑定多个条件,一个
ReentrantLock
对象可以同时绑定对个对象。
有三个线程
T1,T2,T3,
如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的
join()
方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3
调用 T2,
T2
调用
T1)
,这样
T1
就会先完成而
T3
最后完成。
实际上先启动三个线程中哪一个都行,
因为在每个线程的
run
方法中用
join
方法限定了三个线程的执行顺序。
public class
JoinTest2
{
// 1.
现在有
T1
、
T2
、
T3
三个线程,你怎样保证
T2
在
T1
执行完后执行,
T3
在
T2
执行完后执行
public static
void
main
(
String
[]
args
) {
final
Thread t1
=
new
Thread
(
new
Runnable
() {
@Override
public
void
run
() {
System
.
out
.
println
(
"t1"
);
}
});
final
Thread t2
=
new
Thread
(
new
Runnable
() {
@Override
public
void
run
() {
try
{
//
引用
t1
线程,等待
t1
线程执行完
t1
.
join
();
}
catch
(
InterruptedException e
) {
e
.
printStackTrace
();
}
System
.
out
.
println
(
"t2"
);
}
});
Thread t3
=
new
Thread
(
new
Runnable
() {
@Override
public
void
run
() {
try
{
//
引用
t2
线程,等待
t2
线程执行完
t2
.
join
();
}
catch
(
InterruptedException e
) {
e
.
printStackTrace
();
}
System
.
out
.
println
(
"t3"
);
}
SynchronizedMap
和
ConcurrentHashMap
有什么区别?
SynchronizedMap()
和
Hashtable
一样,实现上在调用
map
所有方法时,都对整个
map
进行同步。而
ConcurrentHashMap
的实现却更加精细,它对
map
中的所有桶加了锁。所以,只要有一个线程访问 map,其他线程就无法进入
map
,而如果一个线程在访问
ConcurrentHashMap
某个桶时,其他线程, 仍然可以对map
执行某些操作。
所以,
ConcurrentHashMap
在性能以及安全性方面,明显比
Collections.synchronizedMap()
更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map
时,如果其他线程试图对
map
进行数据修改,也不会抛出ConcurrentModifificationException
。
什么是线程安全
线程安全就是说多线程访问同一代码,不会产生不确定的结果。
在多线程环境中,当各线程不共享数据的时候,即都是私有(
private
)成员,那么一定是线程安全的。
但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到
synchronized
, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会 产生不可预制的结果。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
Thread
类中的
yield
方法有什么作用?
Yield
方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU
占用而不能保证使其它线程一定能占用
CPU
,执行
yield()
的线程有可能在进入到暂停状态后马上又被执行。
Java
线程池中
submit()
和
execute()
方法有什么区别?
两个方法都可以向线程池提交任务,
execute()
方法的返回类型是
void
,它定义在
Executor
接口中
,
而 submit()方法可以返回持有计算结果的
Future
对象,它定义在
ExecutorService
接口中,它扩展了
Executor
接口,其它线程池类像
ThreadPoolExecutor
和
ScheduledThreadPoolExecutor
都有这些方
法。
说一说自己对于
synchronized
关键字的了解
});
t3
.
start
();
//
这里三个线程的启动顺序可以任意,大家可以试下!
t2
.
start
();
t1
.
start
();
}
}
synchronized
关键字解决的是多个线程之间访问资源的同步性,
synchronized
关键字可以保证被它修 饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在
Java
早期版本中,
synchronized
属于重量级锁,效率低下,因为监视器锁(
monitor
)是依 赖于底层的操作系统的 Mutex Lock
来实现的,
Java
的线程是映射到操作系统的原生线程之上的。如果 要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的
synchronized
效率低的原因。庆幸的是在
Java 6
之后
Java
官方对从
JVM
层面对
synchronized
较大优化,所以现在的 synchronized
锁效率也优化得很不错了。
JDK1.6
对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
说说自己是怎么使用
synchronized
关键字,在项目中用到了
吗
synchronized
关键字最主要的三种使用方式:
修饰实例方法
:
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法
:
也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static
表明这是该类的一个静态资源,不管
new
了多少个对象,只有一份)。所以如果一个线程A
调用一个实例对象的非静态
synchronized
方法,而线程
B
需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,
因为访问静态
synchronized
方法占用的锁
是当前类的锁,而访问非静态
synchronized
方法占用的锁是当前实例对象锁。
修饰代码块
:
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结
:
synchronized
关键字加到
static
静态方法和
synchronized(class)
代码块上都是是给
Class
类上 锁。synchronized
关键字加到实例方法上是给对象实例上锁。尽量不要使用
synchronized(String a)
因 为JVM
中,字符串常量池具有缓存功能!
什么是线程安全?
Vector
是一个线程安全类吗?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运 行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。一 个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可 以将集合类分 成两组,线程安全和非线程安全的。Vector
是用同步方法来实现线程安全的
,
而和相似的ArrayList
不是线程安全的。
volatile
关键字的作用?
一旦一个共享变量(类的成员变量、类的静态成员变量)被
volatile
修饰之后,那么就具备了两层语
义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他
线程来说是立即可见的。
禁止进行指令重排序。
volatile
本质是在告诉
jvm
当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读
取;
synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile
仅能使用在变量级别;
synchronized
则可以使用在变量、方法、和类级别的。
volatile
仅能实现变量的修改可见性,并不能保证原子性;
synchronized
则可以保证变量的修改可
见性和原子性。
volatile
不会造成线程的阻塞;
synchronized
可能会造成线程的阻塞。
volatile
标记的变量不会被编译器优化;
synchronized
标记的变量可以被编译器优化。
常用的线程池有哪些?
newSingleThreadExecutor
:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照
任务的提交顺序执行。
newFixedThreadPool
:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达
到线程池的最大大小。
newCachedThreadPool
:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池
大小完全依赖于操作系统(或者说
JVM
)能够创建的最大线程大小。
newScheduledThreadPool
:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任
务的需求。
newSingleThreadExecutor
:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务
的需求。
简述一下你对线程池的理解
(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)合理
利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系
统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Java
程序是如何执行的
我们日常的工作中都使用开发工具(
IntelliJ IDEA
或
Eclipse
等)可以很方便的调试程序,或者是通过打包工具把项目打包成 jar
包或者
war
包,放入
Tomcat
等
Web
容器中就可以正常运行了,但你有没有想过 Java
程序内部是如何执行的?其实不论是在开发工具中运行还是在
Tomcat
中运行,
Java
程序的执行流程基本都是相同的,它的执行流程如下:
先把
Java
代码编译成字节码,也就是把
.java
类型的文件编译成
.class
类型的文件。这个过程的
大致执行流程:
Java
源代码
->
词法分析器
->
语法分析器
->
语义分析器
->
字符码生成器
->
最终
生成字节码,其中任何一个节点执行失败就会造成编译失败;
把
class
文件放置到
Java
虚拟机,这个虚拟机通常指的是
Oracle
官方自带的
Hotspot JVM
;
Java
虚拟机使用类加载器(
Class Loader
)装载
class
文件;
类加载完成之后,会进行字节码效验,字节码效验通过之后
JVM
解释器会把字节码翻译成机器码
交由操作系统执行。但不是所有代码都是解释执行的,
JVM
对此做了优化,比如,以
Hotspot
虚
拟机来说,它本身提供了
JIT
(
Just In Time
)也就是我们通常所说的动态编译器,它能够在运行时
将热点代码编译为机器码,这个时候字节码就变成了编译执行。
Java
程序执行流程图如下:
说一说自己对于
synchronized
关键字的了解
synchronized
关键字解决的是多个线程之间访问资源的同步性,
synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在
Java
早期版本中,
synchronized
属于重量级锁,效率低下,因为监视器锁(
monitor
)是依赖于底层的操作系统的 Mutex Lock
来实现的,
Java
的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在
Java 6
之后
Java
官方对从
JVM
层面对
synchronized
较大优
化,所以现在的
synchronized
锁效率也优化得很不错了。
JDK1.6
对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
说说自己是怎么使用
synchronized
关键字,在项目中用到了
吗
synchronized
关键字最主要的三种使用方式:
修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
。也就是给当前
类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(
static
表明这是该类的一个静态资源,不管
new
了多少个对象,只有一份,所以对该类的所有对象都加了
锁)。所以如果一个线程
A
调用一个实例对象的非静态
synchronized
方法,而线程
B
需要调用这个
实例对象所属类的静态
synchronized
方法,是允许的,不会发生互斥现象,
因为访问静态
synchronized
方法占用的锁是当前类的锁,而访问非静态
synchronized
方法占用的锁是当前
实例对象锁
。
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
和 synchronized 方法一样,
synchronized(this)
代码块也是锁定当前对象的。
synchronized
关键字 加到 static
静态方法和
synchronized(class)
代码块上都是是给
Class
类上锁。这里再提一下:
synchronized
关键字加到非
static
静态方法上是给对象实例上锁。另外需要注意的是:尽量不要
使用
synchronized(String a)
因为
JVM
中,字符串常量池具有缓冲功能!
下面我已一个常见的面试题为例讲解一下
synchronized
关键字的具体使用。
面试中面试官经常会说:
“
单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单利 模式的原理呗!”
双重校验锁实现对象单例(线程安全)
另外,需要注意
uniqueInstance
采用
volatile
关键字修饰也是很有必要。
uniqueInstance
采用
volatile
关键字修饰也是很有必要的,
uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
1.
为
uniqueInstance
分配内存空间
2.
初始化
uniqueInstance
3.
将
uniqueInstance
指向分配的内存地址
但是由于
JVM
具有指令重排的特性,执行顺序有可能变成
1->3->2
。指令重排在单线程环境下不会出先 问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1
执行了
1
和
3
,
此时
T2
调用
getUniqueInstance()
后发现
uniqueInstance
不为空,因此返回
uniqueInstance
,但此 时 uniqueInstance
还未被初始化。
使用
volatile
可以禁止
JVM
的指令重排,保证在多线程环境下也能正常运行。
讲一下
synchronized
关键字的底层原理
synchronized
关键字底层原理属于
JVM
层面。
①
synchronized
同步语句块的情况
public class
Singleton
{
private volatile static
Singleton uniqueInstance
;
private
Singleton
() {
}
public static
Singleton getUniqueInstance
() {
//
先判断对象是否已经实例过,没有实例化过才进入加锁代码
if
(
uniqueInstance
==
null
) {
//
类对象加锁
synchronized
(
Singleton
.
class
) {
if
(
uniqueInstance
==
null
) {
uniqueInstance
=
new
Singleton
();
}
}
}
return
uniqueInstance
;
}
}
public class
SynchronizedDemo
{
public
void
method
() {
synchronized
(
this
) {
System
.
out
.
println
(
"synchronized
代码块
"
);
}
}
}
通过
JDK
自带的
javap
命令查看
SynchronizedDemo
类的相关字节码信息:首先切换到类的对应目录 执行 javac SynchronizedDemo.java
命令生成编译后的
.class
文件,然后执行
javap
-
c
-
s
-
v
-
l SynchronizedDemo.class 。
从上面我们可以看出:
synchronized
同步语句块的实现使用的是
monitorenter
和
monitorexit
指令,其中
monitorenter
指令指向同步代码块的开始位置,
monitorexit
指令则指明同步代码块的结束位置。
当执行
monitorenter
指令时,线程试图获取锁也就是获取
monitor(monitor
对象存在于每个
Java
对象
的对象头中,
synchronized
锁便是通过这种方式获取锁的,也是为什么
Java
中任意对象可以作为锁的
原因
)
的持有权
.
当计数器为
0
则可以成功获取,获取后将锁计数器设为
1
也就是加
1
。相应的在执行
monitorexit
指令后,将锁计数器设为
0
,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等
待,直到锁被另外一个线程释放为止。
②
synchronized
修饰方法的的情况
synchronized
修饰的方法并没有
monitorenter
指令和
monitorexit
指令,取得代之的确实是
ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,
JVM
通过该
ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
为什么要用线程池?
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如 已完成任务的数量。
这里借用《
Java
并发编程的艺术》提到的来说一下使用线程池的好处:
降低资源消耗。
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。
当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系
统的稳定性,使用线程池可以进行统一的分配,调优和监控。
实现
Runnable
接口和
Callable
接口的区别
如果想让线程池执行任务的话需要实现的
Runnable
接口或
Callable
接口。
Runnable
接口或
Callable
接 口实现类都可以被ThreadPoolExecutor
或
ScheduledThreadPoolExecutor
执行。两者的区别在于 Runnable 接口不会返回结果但是
Callable
接口可以返回结果。
备注:
工具类
Executors
可以实现
Runnable
对象和
Callable
对象之间的相互转换。
(
Executors.callable
(
Runnable task
)
或
Executors.callable
(
Runnable task
,
Object
resule
)
)。
执行
execute()
方法和
submit()
方法的区别是什么呢?
1)
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
2)
submit()
方法用于提交需要返回值的任务。线程池会返回一个
future
类型的对象,通过这个
future
对象可以判断任务是否执行成功
,并且可以通过
future
的
get()
方法来获取返回值,
get()
方法会阻塞当前线程直到任务完成,而使用 get
(
long timeout
,
TimeUnit unit
)
方法则会阻塞当前线程一段时间 后立即返回,这时候有可能任务没有执行完。
如何创建线程池
《阿里巴巴
Java
开发手册》中强制线程池不允许使用
Executors
去创建,而是通过
ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽 的风险**
Executors
返回线程池对象的弊端如下:
FixedThreadPool
和
SingleThreadExecutor
: 允许请求的队列长度为
Integer.MAX_VALUE,
可能堆积大量的请求,从而导致
OOM
。
CachedThreadPool
和
ScheduledThreadPool
: 允许创建的线程数量为
Integer.MAX_VALUE
,可能会创建大量线程,从而导致
OOM
。
方式一:通过构造方法实现
方式二:通过
Executor
框架的工具类
Executors
来实现
我们可以创建三种类型的
ThreadPoolExecutor
:
FixedThreadPool
: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。
当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在
一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor
:
方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线
程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool
:
该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数
量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新
的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复
用。
对应
Executors
工具类中的方法如图所示: