一. 多线程
1. 分类
A. Thread
最常用的开启新线程的方式,最终的调用是由Java虚拟机根据不同平台来执行不同的调用,因为
start0
最终是一个native方法。
B. Runnable
通过源码可以得知,Runnable的
run
方法最终是被Thread
中的run
方法执行的。它和Thread
的区别在于可以重用,把有可能重用的代码封装到Runnable
中。
C. ThreadFactory
标准的工厂设计模式,通过工厂设计模式来统一提供
Thread
对象,可以对Thread
对象做统一的处理工作。
D. Executor
线程池。这也是我们在实际当中使用最多的多线程的工具。通过线程池我们可以获取很多内置的应用不同场景下的多线程。
E. Callable
带返回值的异步任务。
2. 使用
-
Thread
new Thread() { @Override public void run() { //do something } }.start();
-
Runnable
Runnable runnable = new Runnable() { @Override public void run() { //do something } }; Thread thread = new Thread(runnable); thread.start();
-
ThreadFactory
ThreadFactory threadFactory = new ThreadFactory() { private AtomicInteger threadCount = new AtomicInteger(0); @Override public Thread newThread(Runnable runnable) { return new Thread(runnable, "Thread-number " + threadCount.incrementAndGet());//++threadCount } }; Runnable runnable = new Runnable() { @Override public void run() { //do something } }; Thread thread = threadFactory.newThread(runnable); thread.start(); Thread thread1 = threadFactory.newThread(runnable); thread1.start();
-
Executor
Runnable runnable = new Runnable() { @Override public void run() { //do something } }; Executor executor = Executors.newCachedThreadPool(); executor.execute(runnable);
内置的常用线程池说明
-
Executors.newFixedThreadPool(threadCount)
获取固定线程数量的线程池。用于处理临时性爆发式任务,比如图片的处理等。 -
Executors.newSingleThreadExecutor()
获取单个线程的线程池,这个用途比较少,比如当取消所有任务的时候可以用这个。 -
Executors.newCachedThreadPool()
带缓存的线程池工具,默认固定线程数量为0,没有线程数量的上限,无活跃60s回收。 -
Executors.newXXXScheduledExecutor()
具有延迟功能的线程池工具。 -
ThreadPoolExecutor
构造函数参数说明:
corePoolSize
:线程池默认固定的线程数量,当线程池空闲时维持的最小线程数。
maximumPoolSize
:线程池允许创建最多的线程数量。
keepAliveTime
:当创建的线程数量超过corePoolSize
的数量后,所创建的线程在指定时间内如果无活动,则被回收。
unit
:参数keepAliveTime
的时间单位。
workQueue
:用于保存execute
方法提交的Runnable
的队列。
threadFactory
:当Executor需要创建新的线程时,由该工厂提供新线程的创建。
-
-
Callable
Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(3000); return "Done!!!"; } }; ExecutorService executorService = Executors.newCachedThreadPool(); Future<String> future = executorService.submit(callable); try { String result = future.get(); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); }
二、线程同步
当多个线程对同一资源进行操作,如对一个变量进行赋值操作,此时就牵涉到了线程安全的问题,就是所谓的线程同步。当我们在编写程序的时候,发现某个变量有可能会被多个线程同时访问和操作的时候,就要考虑到线程同步的问题,所谓的并发编程。接下来说下为什么会出现这种线程安全的问题,如下图:
假如我们自己写了一段程序,运行在主内存中,我们的程序中有一个变量x初始值为0.当线程A想要访问x变量时,会先将变量x拷贝到自己的线程所属的内存中。当线程A修改了x变量的值为1,在合适的时机将修改后的变量值同步到主内存中。线程B再去取主内存中x值的时候已经是修改后的值。
上述过程是没有问题的,这也是我们所期望的。
为什么会这么设计呢?为什么不直接从主内存中取呢?因为当其他线程和主内存频繁的IO操作时,效率是非常低的。而将主内存的值拷贝到线程自己的高速缓存中,再同步到主内存中,这种设计的效率是比直接操作主内存高出几十倍的。
那我们这么解决这种情况下的问题呢?
答案是:线程同步。
在Java中怎样实现线程同步呢?
1. volatile
关键字
private volatile boolean isOpen = true;
将修饰的变量的线程同步性强制打开。线程会以最积极的同步方式进行线程间的同步。使用变量前会先从主内存中同步,修改之后会立即同步到主内存。虽然保证了线程的安全性,但是效率却很低,所以只有当我们需要的时候才去打开。但是volatile
关键字只对原子操作有效,非原子操作无效。例如:
private volatile int x = 0;
private void count(){
x++;
}
在这个例子中volatile
关键字是无效的,原因是因为x++
在实际运行中是分两步的:
1.int temp = x + 1;
2. x = temp;
这个操作是非原子操作。
2. synchronized
关键字
synchronized
关键字的出现完美的解决了volatile
关键字的局限性。
作用:
1. 保证同步性,即是volatile
的特性;
2. 互斥访问,对代码块中的资源进行保护,保证了资源的同步性,即原子操作。
下面我们画一下synchronized
的工作模型。
当线程A访问对象
Test
中的方法A的时候,由于加了synchronized
关键字,线程A会先访问monitor
,询问下是否可以访问方法A,如果可以访问,则直接访问方法A,此时monitor
状态为不可访问。当线程B进行访问方法A的时候,同样也需要先询问 monitor
,此时的monitor
的是不可访问的,所以线程B是不可以访问方法A的,只能等线程A访问结束后,monitor
监视器的状态为可访问时,线程B才可以访问方法A。不同的方法可以加不同的
monitor
,使用synchronized
代码块,传入不同的对象,即可实现不同的monitor
,即synchronized(monitor){}
,如下图:多个monitor
代码实现如下:
private final Object monitorA = new Object();
private final Object monitorB = new Object();
private void methodA() {
synchronized (monitorA){
//do something
}
}
private void methodB() {
synchronized (monitorB) {
//do something
}
}
private void methodC() {
synchronized (monitorB){
//do something
}
}
public void test() {
Thread threadA = new Thread(){
@Override
public void run() {
methodA();
}
};
threadA.start();
Thread threadB = new Thread(){
@Override
public void run() {
methodA();
methodB();
}
};
threadB.start();
Thread threadC = new Thread(){
@Override
public void run() {
methodC();
}
};
threadC.start();
}
- 如果
synchronized
修饰的是方法,则monitor
默认传入的是该方法所在的对象,即this
,代码如下:private synchronized void method() { //do something }
- 如果
synchronized
修饰的是静态方法,则monitor
需要传入静态对象,如:静态变量,**.class等,**.class一般传入对应类的.class,代码如下:
上述代码中传入的class Test{ private static synchronized void method() { //do something } }
monitor
,默认为该静态方法所在类文件的.class
对象,即Test.class
。
上述代码可以传入指定静态的class Test{ private static void method() { synchronized(Test.class){ //do something } } }
monitor
对象,这种常用于单例对象的获取。
这里我们也顺便说下单利对象的写法吧,一般这样写足够安全了,其中如果单利对象初始化对象的时候,需要依赖注入,则要加上volatile
关键字,保证初始化对象的同步性,即初始化完成之后,对象才标记为非空,否则可以不加。public final class SingleMan { private static volatile SingleMan sInstance; private SingleMan(String tag) {} public static SingleMan getInstance(){ if (sInstance == null) { synchronized (SingleMan.class) { if (sInstance == null) { sInstance = new SingleMan("SingleMan"); } } } return sInstance; } }
3. ReentrantLock
可重入锁
这是一个手动锁,上锁和解锁都需要写代码的人去完成,而且异常情况也需要自己处理。我们通过代码来看下它常规的使用。
private final ReentrantLock lock = new ReentrantLock();
private void methodA() {
lock.lock();//上锁
try {
//do something
} finally {
lock.unlock();//不管是否异常,最终都会释放锁
}
}
它和synchronized
的区别在哪?
-
synchronized
是最为关键字出现的,上锁和解锁以及异常处理,JVM已经帮我们做了,而ReentrantLock
不是关键字,上锁和解锁已经异常处理需要我们手动完成。 -
synchronized
不能具体区分出读锁和写锁,而ReentrantLock
可以分别加读锁和写锁,所以相对于synchronized
而言ReentrantLock
锁粒度更细。
下面在看下ReentrantLock
的读锁和写锁的常规使用,举个简单的栗子。
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
private int x = 0;
private void count() {
writeLock.lock();
try {
x++;
} finally {
writeLock.unlock();
}
}
private void printNumber() {
readLock.lock();
try {
System.out.println("The number is " + x);
} finally {
readLock.unlock();
}
}
以上代码,当执行count
方法时,其他线程是不可以对x
进行写操作,但是可以进行读操作,同样,当执行printNumber
方法时,其他线程是不可以对x
进行读操作,但是可以进行写操作,因为读写锁是分离的。
这里对ReentrantLock以及锁机制不再更深一步的了解,先点到为止,再加上个人能力有限,如果后面有更深入的理解,再加入进来。
相关问题
-
进程和线程有什么区别?
线程是运行在进程中的,每个操作系统中有许多个进程,每个进程都是相互独立的。
举个通俗的例子,这个世界就是一个大的操作系统,每一个家庭代表着一个进程,而每个家庭中的人代表着每一个线程。每个家庭可以进行沟通,而每个进程也遵循一定的协议进行沟通,每个人可以协同完成某一任务,每个线程也可以通过协同合作来完成某一个任务。
往深的说,线程间共享资源,进程间不共享。而且这俩都不是一个概念,不能进行比较,共同点就是可以同时并行。 -
死锁
死锁只出现在锁关系比较复杂的情况下,即锁嵌套,单个锁是不会出现的。如下代码则会出现死锁的情况:public void method1(){ synchronized(monitor1){ //do something synchronized(monitor2){ //do something } } } public void method2(){ synchronized(monitor2){ //do something synchronized(monitor1){ //do something } } }
方法1和方法2互相持有
monitor1
和monitor2
的所对象,就会出现,方法1中执行到synchronized(monitor2){}
这句代码时,会等待方法2释放monitor2
锁对象,同样,方法2执行到synchronized(monitor1){}
这句代码时,会等待方法1释放monitor1
锁对象,二者互相等待对方释放锁对象,就死等,这一等就是一辈子。 -
乐观锁和悲观锁
在后端开发中,经常会遇到这种问题,当从数据库中读出数据后,然后啪啪啪进行一顿业务操作,之后再将数据写回数据库中,这时出现以下情况:- 在自己从数据库中读数据之后到写数据之前,数据库中的数据可能已经被其他伙伴修改过了。
遇到这种情况,做法如下:
-
假设我们读的时候不上锁,写回数据库的时候再检查数据是否发生了改变,之后根据具体在写回数据库,写的过程是一定加锁的,这个处理方式即为乐观并发控制,所谓乐观锁。
-
假设我们在读数据之前,就先上锁,不允许其他伙伴进行读写操作,直到自己将锁释放后,别的伙伴才可以读写,这个处理方式即为悲观并发控制,所谓悲观锁。
以上两种处理方式皆为处理并发操作的思想。
由于个人能力有限,如有错误之处,还望指出,我会第一时间验证并修改。
理解事物,看本质。共勉。
网友评论