------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
多线程(线程安全和通信)
一、线程安全问题
线程安全问题,详细来说,先看一个程序。
If (tick>0)
System.out.println(Thread.currentThread()+"..sale..."+tick- -);
这是继承了Runnable接口的子类的run方法中的一部分代码。我们知道,CPU在每个时间段,仅会执行一个程序,当我们建立多个线程来运行这一部分代码的时候,问题就发生了。假如有两个线程,线程A和线程B,当线程A执行时,这是tick的值是1,这是可以通过if语句,但是在下一条输出语句之前被暂时冻结了(注意输出语句中的tick执行完就会减1),这时线程B也进来了,但tick还是1,所以也通过if语句了,同时也在输出语句之前冻结了。这个情况之后,CPU回来执行线程A,继续执行下面的输出语句,之后tick的值就变为了0,这时其实这个if语句就该停止了,但是CPU又开始执行线程B了,所以又往下执行了一次输出语句,tick已经变了,所以最后tick编程了-1。这是我们设计程序里不合理的,这就是线程的安全问题。
简单的说:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一线程就参与进来执行,导致线程共享数据错误。
解决办法:
对多条操作共享数据段语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
这就引入了同步代码块:
synchronized(对象)
{ 需要被同步的代码 }
同步代码块传入的是对象,注意一般来说只要是对象即可。示例程序如下:
Object obj = new Object();
synchronized(obj)
{ If (tick>0)
System.out.println(Thread.currentThread()+"..sale..."+tick- -);
}
同步代码块中的对象,就相当于一个锁,当有一个线程进入到这个代码块中,就相当于把这个锁给锁上了,外面进不来,只有当这个线程执行完同步代码块中的程序,才会把这个锁给打开。另一种描述是,进入代码块的线程,持有这个锁,当线程执行完程序后,才会释放这个锁,使得后面的线程可以进入。同步代码块中同一时间只会有一个线程。
总结:
1、 多线程存在安全问题,多个线程操作同一条共享数据时会发生错误。
2、 错误原因是:一个线程对多条语句只执行了一部分,这时另一个线程就进来执行,导致线程共享数据错误。
3、 处理多线程安全问题用同步代码块,格式记住了。
4、 同步代码块要注意的是,一、传入的是对象,可以是任意对象。二、代码块中寸放大是需要被同步的代码,不要全部放入。三、同步代码块中仅会有一个线程再执行。
5、 同步的前提:一、必须要有两个或者两个以上的线程。二、必须是多个线程使用同一个锁。
6、 同步代码块的优点:解决了多线程安全问题。弊端:因为多加判断,较为耗费资源。
二、线程安全的各种情况
我们在写程序时,如果用到线程安全问题,该怎么去判断,怎么去具体的给需要的代码加上同步代码块?
根据我们已学的去判断的话,多线程使用的我们自己定义类中run方法中的程序,这部分程序时我们建立多线程时,线程实际执行的部分,那么就可以判断,多线程运行的代码在run方法中。然后我们不能把整个run方法都写入同步代码块中,那样岂不是成了单线程,所以要判断哪些是共享数据,一般来说,成员变量都是共享数据,只要是run方法中用到的成员变量,那么这些变量就是共享数据,但是共享数据如果只有一句的话,比如成员变量x,打印语句System.out.println(x++),这样一句语句,里面有共享数据,当多个线程执行到这部分时,不管哪个线程或停或执行,一句话过后,x都会改变,不会有x在两个线程的值是一样的。所哟要看多语句,比如:
void add(int x) {
sum= sum+x;
System.out.println(sum);
}
当有这样同样的sum共享数据的时候,线程A在打印语句之前休眠,之后线程B进来再执行求和语句,那么sum此时就变了,线程A在执行是就会打印后面的结果,而不是线程A本身执行完的结果。所以对于多线的判断总结一下:
对于多线程安全问题的判断:
1、 明确哪些代码是多线程运行代码(一般是run方法中的代码)
2、 明确共享数据(一般是用到的成员变量)
3、 明确多线程运行代码中哪些语句是操作共享数据的(注意同一数据,大于两条语句的)。
通过以上的判断,我们就可以再相应的语句上加上同步代码块,这样坐到精准和细致。
Synchronized也可以直接加上某个函数上,形式为:
public synchronized void add(intx){ }
这样同步代码就有两种形式:
一、 同步代码块
二、 同步函数
同步代码块有锁,同步函数也有锁。因为函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。如果同步函数被静态修饰的话,就为静态同步函数,而静态同步函数的锁是Class对象,比如:
class Demo implements Runnable{
public static synchronized void add(int x){ }
}
看上述程序,当静态进内存的时候,内存中没有本类的对象,但是一定有该类对应的字节码文件对象,就是类名.class 对象的类型就是Class 。使用的锁就是该方法所在的字节码文件对象。则,此时的锁即是Demo.class
同步可以再单例设计模式中的懒汉式中可以用到,详情再查看设计模式之单例设计模式。
三、同步小点:死锁
死锁,就是A有一个锁,B有一个锁,A那这自己的锁想进B里面,B也拿着自己的锁,想进A里面,那么谁都不放锁,那么就会死锁。
一般程序里情况是:同步里嵌套同步。
我们要避免这中情况发生。
四、线程间通讯
线程间通讯就是多个线程在操作同一个资源,但是操作的动作不同。
看一个示例代码:
class Res {
数据变量;
}
class Input implements Runnable{
public void run(){
操作Res中的内容:给Res中输入数据。
}
}
class Output implements Runnable{
public void run(){
操作Res中的内容:读取Res中的数据
}
}
classTestForYou{
public static void main(String[] args){
主函数中建立输入和输出线程。
}
}
看上面的程序,应该明白什么事线程间通讯,就是操作同一数据,确实不同动作。当然,数据的输入和数据的输出,也得达到我们想要的效果。当然,会出现线程安全问题,按照多线程安全问题判断和同步前提两个规则来解决即可。
等待唤醒机制
wait():等待 锁.wait()
notify():唤醒 锁.notify()
notifyAll():全部唤醒
它们都是使用在同步中,因为要对持有监视器(锁)的线程操作。所以,都要使用在同步中,因为同步才具有监视器(锁)。
为什么这些方法定义在Object类中?因为这些方法在操作同步线程时,都必须要表示它们锁操作线程持有的锁,只有统一锁上的被等待线程,可以被同一个锁上的notify唤醒。也就是说等待和唤醒必须是同一个锁。而锁可以是任意对象,所以只能定义在Obiect类中。
情况一:之前,我们操作共享的数据的是两个线程,一个线程对共享数据的操作动作是输入,一个线程对共享数据的操作动作是输出,当只有两个线程的时候,我们是可以进行普通的方法进行同步,使得不会出现线程安全问题。
我们想输入一组数据后,在去除一组数据,这时就需要某个线程执行完,等待一段时间,这里就用到三个个,一是设定一个标记,二是用到wait(),三是notify()。比如下面这段程序中的一部分:
while(true){
synchronized(r){
if(r.flag)
try{r.wait();}catch(Exceptione){}
if(x==0){
r.name ="lili";
r.sex ="woman";
}
else{
r.name ="wang wu ";
r.sex =" xxxx MAN";
}
x=(x+1)%2;
r.flag = true;
r.notify();
}
}
r为共享数据的对象,name和sex为共享数据中的成员变量。
把flag标记初始化为false,在同步代码块中进行判断。走一遍顺序,if(r.flag)为假,那么就执行下面的输入数据的语句(x初始化为0),到后面改变flag的值为true,然后唤醒另一输入语句中的等待的线程,这时在回去判断flag的值为真,那么此线程就会等待,另一输出线程就会执行。看输出部分代码:
while(true){
synchronized(r){
if(!(r.flag))
try{r.wait();}catch(Exceptione){}
System.out.println(r.name+".."+r.sex);
r.flag = false;
r.notify();
}
}
需要理解的是,线程等待是在一个线程池中,当notify要唤醒线程时,是要唤醒最早的那个等待的线程,这是默认方法。我们可以通过唤醒和锁有关的线程,来唤醒我们想要唤醒的行程,那么看上面程序,同步代码块的锁是共享数据对象r,所以,wait()和notify()前面加上锁,就相当于打上了标记,这样当程序运行时候,就会唤醒同一个锁的等待线程。
情况二:情况一是两个线程共享一个数据,如果多个线程共享一个数据呢?还是上面示例程序,我们建立两个输入数据线程和两个输出数据线程,按照程序执行顺序来说,某个时刻上,输入数据的连个线程,后一个线程可能会覆盖前一个线程的数据,意思就是说,当我们生产了两双鞋,但商家只卖出一双,或者是生产了一双鞋,商家卖出两双,这样显然是不合理的。我们可以再判断上做改动,使得每个线程被唤醒时,可以再次判断标记flag,把if判断改为while循环:whle(flag)。这样线程被唤醒时,就可以多次判断,不会出现上述情况。但另一个问题就产生了,因为执行权是随机的,那么这个修改的程序,所有线程有可能就会全部等待,这时,我们就用到另一关键字,notifyAll,就是唤醒当前所有线线程,这就避免了线程全部等待。
JDK1.5新特性:多线程新解决方案:Lock、Condition
JDK1.5中,有一个接口Lock,这个替代了synchronized,同时Lock使用到了一个Condition接口,这个接口替代了Object中的wait(),notify(),notifyAll()。
Lock中有lock和unlock方法,就是一个拿锁,一个放锁。
Condition中提供了await(),就相当于wait()。还提供了signal和signalAll方法,就相当于notify和notifyAll。具体实现方式看如下程序:
private Lock lock= new ReentrantLock();
private Condition condition = lock.newCondition();
………………………….省略部分代码
publicvoid run() throwsInterruptedExceprion {
while(true){
lock.lock();
try{
while(!(r.flag))
condition.await();
System.out.println(r.name+".."+r.sex);
r.flag= false;
condition.signal();
}
finally{
lock.unlock();
}
有上述程序可知,我们创建一个Lock接口的子类对象,使用lock方法中的newCondition()来创建一个Condition的引用对象,这样其实就是持有lock锁的一个对象,而这个对象里面有await() , singal() ,signalAll(),方法。在执行程序部分,lock.lock()方法来持有一个锁,在判断标记后面,用condition.await()是当前线程等待(这里会有InterruptedException抛出,所以要用try加测,但不处理,我们也抛出异常),然后用condition.signal(); 等待的线程,把lock.unlock()放到finally块中,是使解锁动作一定要执行。这就是程序中Lock类体系替代原有同步代码块的方式。其实,在这一部上也就是把同步代码块替换成了我们JDK1.5的新技术,运行起来还是会有问题。因为,condition.signal()唤醒的也是最先等待的线程,即是是换成condition.signalAll()唤醒全部等待线程,但是以前的那个线程全部等待的情况依旧会发生。这里,condition就发挥作用了。看下面程序:
private Lock lock= new ReentrantLock();
private Condition condition_in = lock.newCondition();
private Condition condition_out = lock.newCondition();
………………………….省略部分代码
publicvoid run() throwsInterruptedExceprion {
while(true){
lock.lock();
try{
while(!(r.flag))
condition_in.await();
System.out.println(r.name+".."+r.sex);
r.flag= false;
condition_out.signal();
}
finally{
lock.unlock();
}
我们建立了两个Condition的对象,一个是为输入数据的,一个是为输出数据的,因为同样从Lock中建立的,所以持有同样的锁。当我们执行程序时,condition_in就会是当前线程等待,就是是此时输入线程等待,而输出方(程序格式和输入方相反),有一个condition_out会使输出方线程等待。与之前的一个Condition对象不同的是,当执行到唤醒操作的时候,输入方程序为:condition_out.signal(),这时只会唤醒condition_out对象相关的等待线程,就是说,两个Condition对象,持有同一个锁,但相当于分别给输入和输出方做了标记,值等待或只唤醒固定对象所对应的线程。
停止线程
我们如果需要停止线程,如何动作。
以前的代码是使用stop方法,但是这个方法已经过时了。
只有一种,run方法结束。因为我们建立线程的时候,就是调用的run方法,而run方法中运行的代码通常是循环结构,只要控制住循环,就可以让run方法结束,就是结束了线程。
所以1、定义循环结束标记,2、使用interrupt(中断)方法。注意点是,interrupt方法是使线程结束冻结状态,回到运行状态中来。程序如下:
privateboolean flag = true;
public void run() {
while(flag){
System.out.println("luanqi ba zao");
}
}
publicvoid setFlag() {
flag = false;
}
在一个方法中,或是其他方法,把循环标记改变即可,是循环结束,那么线程就结束了。
但是要注意个特殊的情况:
privateboolean flag = true;
publicvoid run(){
while(flag){
try
{
wait();
}
catch(InterruptedException e)
{
System.out.println(".....Wait..A.....");
}
System.out.println("................");
}
}
publicvoid setFlag(){
flag = false;
}
注意这个情况,当两个线程执行时,可能两个线程都在wait出等待了,此时,即便是我们改变了标记flag的值,线程也不会结束。就是说:当线程处于冻结状态,就不会读取标记,那么线程就不会结束。
这里我们就需要用到Thread中的interrupt方法,这个方法就是强制使线程接触冻结状态,但不是使线程停止,而是使线程回复运行状态。
在主函数中调用interrupt方法,使冻结的线程回到运行状态,既然回到运行状态,那么就好解决线程停止问题了。因为interrupt会抛出一个interruptException异常,所以有两个方法可以解决。一是在主函数中,在使用interrupt方法之前改变标记。二是在异常处理中,改变标记,因为既然抛出异常了,就说明我们强制中断冻结状态想要使线程停止,那么就在catch语句中改变标记即可。
多线程中一些其他方法
1、 setDaemon守护线程
守护线程就相当于后台线程,我们写的线程都是前台线程,这个方法就是当前台线程结束的时候,守护线程就会自动结束。拿毕老师的话来说,就是雅典娜over了,那圣斗士就都失业解散了。
注意的是使用方法:在建立线程之前设定。使用方法如下。
t1.setDaemon(true);
t1.start();
2、 Join方法
等待该线程终止(会抛出一个异常)。
那个例子来理解:
t1.start();
t1.join();
t2.start();
上例,当执行到join方法时,主线程就把执行权给到t1线程了,直到t1线程执行完毕,主线程才会获得执行权,继续下面开启线程的方法。如果这这种情况:
t1.start();
t2.start();
t1.join();
先建立了两个线程,当执行到join方法时,主线程就会交出执行权,此时,有两个线程t1和t2都可以执行,所以两个线程会交替执行。直到当t1线程执行完毕后,主线程才会获得到执行权。
简单来说:当A线程执行到了B线程的join()方法时,A就会等待,等待B线程都执行完,A才会执行。
Join可以用来临时加入线程执行。
3、 优先级和yield方法
1)优先级
线程组概念,就是A个线程开启了多个线程,那么这多个线程就属于A这个线程组。
通过Thread类中的toString方法,我们可以查看每个线程的优先级,即线程组。对于优先级,一共10级,默认是5级,就是说,你级数越高,CPU执行你的概率越高,当然仅是概率。我们可以用setPriority(int 级数)方法继续设置。
2)yield方法
暂停当前正在执行的线程对象,并执行其他线程。方法时静态的。
就是暂停一下,这样使得其他线程有机会执行。
总结:
1) 对于线程安全问题,第一个解决方案是synchronized同步代码块,对线程运行代码中操作共同数据的语句进行同步,提高线程安全性。要注意的是,同步代码块的监视器即锁,是任意对象。
2) 同步代码块注意事项:一是有两个前提,1、多个线程,2、同一个锁。这样一定要注意,当线程出错时,一定是这两点。
3) 对什么地方加同步代码块的判断:1、明确哪些代码是多线程运行代码,2、明确哪些是共享数据,3、明确多线程运行代码中哪些语句是操作共享数据的。
4) 同步函数的锁是this,静态同步函数的锁是所属类的字节码,即Class
5) 同步中嵌套同步会形成死锁,要避免这样的编写。
6) 线程通信中(同步代码块方法),注意只有两个线程共享数据和多个线程共享数据时的区别。只有两个线程时,if判断标记即可,notify唤醒之前冻结的线程;多个线程,while循环判断标记,notify全部唤醒。
7) 线程通信中(Lock方法),用Lock方法注意的是,多个线程要建立多个Condition对象,以用于唤醒除本方线程的其他线程。其中await,signal都是Condition对象里的方法,用Condition兑现调用。开头的lock.lock(),在最后一定要解锁unlock,因为等待会有异常,所以要try-finally,在finally中解锁。
8) 停止线程中注意特殊情况,多个线程等待,循环不会结束,就是读不到结束标记。我们可以用interrupt方法中断冻结状态的线程,在其在判断标记钱或是异常处理中(interrupt会报出异常)改变标记,结束循环即可。
9) 守护线程用于建立线程钱,会在前台线程终止后立即终止。Join方法当A线程执行到了B线程的join()方法时,A就会等待,等待B线程都执行完,A才会执行。优先级有10级,用setPriority(int 级数)来设定,但只是CPU执行可能性提高。Yield方法时暂停一下线程,会释放执行权,减缓线程执行频率,临时释放,主要作用其实就是可以让其他线程有机会执行,有平均运行的效果。
10) 开发时怎么写线程,注意下面代码:
classDemo
{
public static void main(String[] args)
{
循环代码1;
循环代码2;
}
}
两个循环代码,可能是有很大数的循环,但是当单线程运行时候,尤其是循环代码1里面数字特别大,那么循环代码2就可能执行不到,那么我们这时间就可以用多线程。简单来说,就是某些代码要同时运行时,就用多线程代码进行封装。看程序:
classDemo {
public static void main(String[] args) {
new Thread(){
public void run(){
循环代码1
}
}.start();
Runnable r = new Runnable(){
pubilicvoid run(){
循环代码2;
}
}
Thread t = new Thread(r);
r.start();
}
}