(1) 异步处理,例如:发微博、记录日志等;
(2) 分布式计算
(3) 定期执行一些特殊任务:如定期更新配置文件,任务调度(如quartz),一些监控用于定期信息采集等
(4) TOMCAT处理多用户请求。
(5) 针对特别耗时的操作。多线程同步执行可以提高速度。例如:定时向大量(100w以上)的用户发送邮件。
问题一:上下文切换。
并发不一定快于串行,因为会有切换上下文的开销。【切换上下文:单核并发时,cpu会使用时间片轮转实现并发,每一次轮转,会保留当前执行的状态】。
解决上下文切换开销的办法:
无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
·CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
·使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
·协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
问题二:死锁。
死锁是一个比较常见也比较难解决的问题,当多个线程等待同一个不会释放的资源时,就会发生死锁。避免死锁可以参考下面的思路。
避免死锁的方法:
1. 避免一个线程同时获取多个锁。
2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
问题三:资源限制。
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1M每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s
解决资源限制思路:
1. 对于硬件资源限制,可以考虑使用集群并行执行程序。
2. 对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
实现多线程基本的实现方式就是如下两种:
继承Thread类;
实现Runnable接口;
实际使用时,会用到线程池,还会用spring管理线程池,下面使用多线程完成几个小例子。
示例一:多线程使用reentrantLock实现交替打印奇数偶数,代码见压缩包:
示例二:4个线程,两个存钱,两个取钱
示例三:spring管理线程池配置
终止线程有三种方式:
(1)使用退出标志,run()执行完以后退出【抛出异常或者return】
(2)使用stop强行停止线程,不推荐,会导致当前任务执行到一半突然中断,出现不可预料的问题;而且stop和suspend以及resume一样是过期作废的方法
(3)使用interrupt中断线程
interrupt()方法不会真的停止线程,而是会记录一个标志,这个标志,可以由下面的两个方法检测到。
Thread.interrupted()测试当前线程是否停止,但是他具有清除线程中断状态功能,如第一次返回true,第二次调用会返回false
Thread.isInterrupted(),仅返回结果,不清除状态。重复调用会结果一致
基于上面的逻辑,可以根据标志来在run()里面状态,然后再使用interrupt()来使代码停止,停止代码可以使用抛出异常的方式。
如果在sleep里面抛出异常停止线程,会进入catch,并清除停止状态,使之变成false;
stop()暴力停止,已经被作废,建议不使用;
使用stop的方法带来的问题:1.执行到一半强制停止,可能清理工作来不及;
2.对锁定的对象进行了解锁,导致数据不同步,不一致。
return方法停止线程:
其实就是使用 打标记+return 替换 打标记+抛异常
# 暂停线程 与 恢复线程
suspend()暂停,resume()恢复,已经被弃用,
缺点:
1. 独占,使用不当很容易让公共的同步对象独占,使得其他线程无法访问。
2. 不同步:线程暂停容易导致不同步。
yield():作用是放弃当前cpu资源,将他让给其他任务去占用cpu;但是放弃的时间不确定,有可能刚放弃,马上又获得cpu时间片;直接在run方法里面使用即可。
多个线程可以设置优先级。
优先级设置:
setPriority()方法;分为1-10 10个等级,超过这个范围,会抛出异常。
java线程优先级可以继承,A线程启动B线程,那么B与A的优先级是一样的。
优先级高的绝大多数会先执行,但结果不是百分之百的。
在run里面执行的方法,如果是同步的,则不会有线程安全问题,使用synchronized关键字即可保证同步。
synchronized持有的锁是对象锁,如果多个线程访问多个对象,则JVM会创建多个锁。【多个对象,多个锁,此处对象是指加了synchronized关键字的方法所在的类也就是创建线程时传入的对象,例如:
Thread a = new Thread(object1);
Thread b= new Thread(object2);
a.start();
b.start();
这种情况下线程a和b持有的是两个不同的锁。
# 赃读:
读取全局变量时,此变量已经被其他线程修改过了,就会出现赃读。
# synchronized实际上是对象锁。
现有A,B两个线程,C对象,C拥有加了synchronized关键字的方法X1()和X2(),以及未加synchronized关键字的X3()方法。
当A线程访问X1方法时,B线程想访问X1,必须等待A执行完,释放对象锁;
当A在访问X1,B想访问X3(),无需等待,直接访问。
当A在访问X1,B想访问X2(),需要等待A执行完。
# synchronized锁重入
在synchronized方法内,调用本类的其他的synchronized方法时,总是可以成功。
如果不可重入的话,会造成死锁;
可重入锁,支持在父子类继承的环境:子类可以通过"可重入锁"调用父类的同步方法。
#异常会释放锁
当一个线程执行出现异常,会释放他所持有的所有锁。
#同步不具有继承性
父类中A()方法是synchronized的,子类中的A方法,不会是同步的,需要手动加上。
#synchronized同步语句块
synchronized(this){
...同步的代码块...
}
synchronized声明方法的弊端:
A线程调用同步方法执行长时间任务时,B线程需要等待很久。
解决办法:可以使用synchronized同步语句块。
synchronized可以修饰代码块。使用synchronized修饰需要保持同步部分代码,其余部分异步,借此提高运行效率。
#synchronized代码块间的同步性
A对象,拥有X1和X2两个synchronized同步代码块,
那么,B线程在访问X1时,C线程也无法访问X2,需要等待B线程释放对象锁。
此处与synchronized修饰方法时一样。他们持有的都是对象锁。
#任意对象作为监视器
synchronized修饰的代码块时,如果传入this,则会监视当前对象,加锁时会对当前整个对象加锁;
例如:
对象A有方法X1(),X2(),如果在X1和X2里有一段同步代码块,并且synchronized(this)传入的都是this对象,那么在B线程访问X1的同步代码块时,C线程也无法X2的同步代码块。
如果传入的不是this,而是另外的对象,则C可以访问X2的同步代码块。
*要保证传入其他监视对象时的成功同步,必须保证在调用时,监视对象是一致的,不能每次都new一个监视对象,否则会导致变成异步的。*
#脏读问题
有时候,仅仅使用synchronized修饰方法,并不能保证正确的逻辑。
比如,两个synchronized修饰的方法add()与getSize(),他们分别是对list进行读与写的操作,此时两个线程先后调用这两个方法,会导致结果超出预期。
解决:
add()方法中,synchronized改成去修饰代码块,并且传入监视对象list;
synchronized(list){
--- add ---
}
#静态同步synchronized方法,与synchronized(class)代码块
synchronized加在static静态方法上,就是对当前.java文件对应的class类进行持锁。
synchronized static等同于synchronized (object.class) 可以对该类的所有对象起作用,
*即:即使需要new不同的对象,也可以保持同步*
#String的常量池特性
一般不使用String变量来作为锁的监视对象,当对一个String变量持有锁时,如果两个访问线程传入的String变量值一样,会导致锁不被释放,其中一个线程无法执行。
可以使用对象来存储相应的变量解决此问题。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
#volatile保证有序性
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
可能上面说的比较绕,举个简单的例子:
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
那么我们回到前面举的一个例子:
//线程1:
context =loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。
#与synchronized对比
1. volatile是线程同步的轻量实现,只能修饰变量,性能高于synchronized
2. volatile保证可见性,不保证原子性【一旦其修饰的变量改变,其余的线程都能发现,因为会强制从公共堆栈取值】,synchronized保证原子性,间接保证可见性,因为他会将私有内存和公共内存的值同步
例如:i++操作,实际上不是原子操作,他有3步:
(1).从内存取i值
(2).计算i的值
(3).将i的新值写到内存
多个线程执行时,使用volatile,可能导致数据脏读,进而出现错误。
3. 多线程访问volatile不会阻塞,而synchronized会
4. volatile是解决变量在多个线程之间的可见性,synchronized是保证多个线程之间资源的同步性。
# volatile的实现原理
1.可见性
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。
如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。
2.有序性
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
# volatile的应用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
下面列举几个Java中使用volatile的几个场景:
① .状态标记量
volatile booleanflag = false;
//线程1
while(!flag){
doSomething();
}
//线程2
public voidsetFlag() {
flag = true;
}
根据状态标记,终止线程。
②.单例模式中的doublecheck
class Singleton{
private volatile static Singleton instance= null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
为什么要使用volatile 修饰instance?
主要在于instance= new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
1.给 instance分配内存
2.调用Singleton 的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步 instance就为非 null 了)。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
# 1.等待通知机制:
wait使线程暂停,而notify使线程继续运行。还有notifyAll()方法。
wait()和notify(),两个方法来实现等待通知机制;
注意:(1)两个方法在调用时都需要持有当前对象的对象锁,所以都只能在同步代码块或者同步方法里面调用,如果不是会抛出异常。
# wait:
(2)wait方法会将当前线程置入“预执行队列”,并在wait()所在代码行停止执行,直到接到notify(),或者被中断;
(3)执行wait()后,当前线程释放锁;
#notify:
(1)如果多个线程在wait,那么会由线程规划器,挑选一个执行notify,并使他获取该对象的对象锁;
(2)noitfy执行之后,当前线程不会立马释放该对象锁,wait状态的线程也不能立马获得该对象锁,要等执行notify()方法的线程将程序执行完,也就是退出synchronized代码块之后才会释放锁,并让wait获得。
(3)多个wait的线程,第一个获取到notify并执行完之后,其余的wait状态的线程如果没有被通知,还是会一直阻塞。
#wait之后自动释放锁,notify之后不会立马释放锁
#interrupt方法与wait
当线程在wait状态时,调用对象的interrupt()方法,会抛出异常。
(1)执行完同步代码块之后,会释放当前对象的锁
(2)执行同步代码块过程中,抛出异常也会释放锁
(3)执行wait()之后,也会释放锁
#wait(long)
执行wait(5000)后,首先会等待5秒,如果5秒内没有收到通知,会自动唤醒线程,退出wait状态。
#通过管道进行线程间通信
4个类进行线程间通信:
(1)字节流:PipedInputStream和PipedOuputStream
(2)字符流:PipedReader和PipedWriter
使用语法:
输出:PipedOuputStream
PipedOuputStream out;
out.write();
out.close();
#join方法
在主线程中调用子线程的join方法,可以让主线程等待子线程结束之后,
再开始执行join()之后的代码。
join可以使线程排队运行,类似于synchronized的同步;区别在于join在内部使用wait()等待,而synchronized使用对象监视器原理同步。
#注意
在join过程中,如果当前线程对象被中断,则当前线程出现异常,子线程会继续运行;
#join(long)
long参数是设定等待时间,使用sleep(long)也可以等待,但二者是有区别的:
join(long),内部是使用的wait(long),等待时会释放锁;
sleep(long)等待时不会释放锁。
#ThreadLocal
变量值的共享可以使用public static;
如果想让每个线程都有自己的共享变量。可以使用ThreadLocal;ThreadLocal可以看做全局存放数据的盒子,盒子中可以存储每个线程的私有数据;
使用时,只需新建一个类继承ThreadLocal即可实现,不同的线程在这个类中取到各自隔离的变量。
#InheritableThreadlocal
InheritableThreadlocal可以在子线程中取得父线程继承下来的值。
使用注意:如果子线程取得值的同时,主线程将值进行了修改,那么取到的还是旧值。
ReenTrantLock可以和synchronized一样实现多线程之间的同步互斥,ReenTrantLock类在功能上还更加强大,有嗅探锁定,多路分支通知等。
使用:
privateLock lock = new ReenTrantLock();
try{
//加锁
lock.lock();
//解锁
lock.unlock();
}catch{
}
#ReenTrantLock结合Condition实现等待/通知
功能上与synchronized结合wait/notify一样,而且更加灵活;
一个Lock对象可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的condition中,从而可以有选择性的进行线程通知,在线程调度上更加灵活。
而在wait/notify时,被通知的线程是JVM随机选择的,不如ReenTrantLock来得灵活。
synchronized相当于整个lock对象中只有一个单一的condition,所有的线程都注册在它上面,线程开始notify时,需要通知所有的waitting线程,没有选择权,效率不高。
#使用
使用之前,必须使用lock.lock()获取对象锁。
privateCondition condition = lock.newCondition();
try{
condition.await();
}catch{}
其实使用上wait()/notify()/notifyAll()相当于Condition类
里面的await()/signal()/signalAll()
wait(longtimeout)相当于await(long time,TimeUnit unit)