Java多线程

来源:互联网 发布:云计算是什么工作 编辑:程序博客网 时间:2024/05/30 19:33

------- java学习型技术博客、期待与您交流 ----------

JAVA 基础总结之多线程

进程与线程:

要学习多线程,首先必须明确进程和线程的概念:

进程:

        进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程:

        线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

进程和线程的区别:

       线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定。线程的运行中需要使用计算机的内存资源和CPU

JAVA中线程的两种创建方式:

方式1:继承Thread ,由子类复写run方法。

创建步骤:

        1定义类继承Thread类;

        2,复写run方法,将要让线程运行的代码都存储到run方法中;

        3,创建Thread类的子类对象,创建线程对象;

        4,调用线程的start方法,开启线程,并执行run方法。

      (作用:启动线程并调用run方法)

代码体现:

  class Demo extends Thread//创建一个类Demo继承Thread类  {  public void run()//复写run方法  {  for(int x=0; x<60; x++)  System.out.println("demo run----"+x);  }  }  class ThreadDemo   {  public static void main(String[] args)   {  Demo d = new Demo();//创建一个Demo对象,创建一个线程对象。  d.start();//开启线程并执行该线程的run方法。  //d.run();//仅仅是对象简单的方法调用。而线程创建了,并没有开启。  }  }

方式2:通过实现Runnable接口:

创建步骤:

        1,定义一个类实现Runnable接口;

        2,复写Runnable接口中的run方法, 将线程要运行的代码存放在该run方法中。

        3,通过Thread类创建线程对象;

        4,将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数。

        5,用Thread对象的start方法。开启线程,并运行Runnable接口子类中的run方法。

             原因是run方法所属的对象是Runnable接口的子类对象,想要线程运行指定对象的run方法,就必须指定该run方法所属的对象。

代码体现:

//定义一个类实现Runnable接口class Demo implements Runnable{//覆盖Runnable接口的run方法public void run(){for(int x=0;x<60;x++)System.out.println("demo run-----"+x);}}class ThreadDemo{public static void main(String[] args){//创建Runnable接口的子类对象Demo d = new Demo();//通过Thread类创建线程对象,将Runnable接口的子类对象作为参数传递给Thread的构造函数Thread t = new Thread(d);//调用Thread类的start方法开启线程t.start();}}

两种创建方式的比较:

一、通过继承thread类方式:

优点

        编写简单,run方法的当前对象就是线程对象,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。

缺点

        由于java只支持单继承,而线程类已经继承了Thread类,所以不能再继承其他的父 类。

 

二、通过实现Runnable接口方式:

优点:

        线程类只是实现了Runnable接口,线程体run()方法所在的类可以从其它类中继承 一些有用的属性和方法。可以多个线程共享同一个目标对象,适合多个相同线程来处理 同一份资源;可以将CPU,代码和数据分开,形成清晰的模型。

 

缺点:

        代码相对继承Thread类的方法较为复杂;不能直接访问当前线程,必须使用 Thread.currentThread()方法。

 

多线程的安全问题:

示例:卖票程序,多个窗口(线程)共卖1000张票

class Ticket implements Runnable{private  int tick = 1000;public void run(){while(true){if(tick>0){  //sleep方法会产生异常,但是在此程序中因为是实现的接口,异常只能try不能抛try{Thread.sleep(10);}catch(Exception e){}  //打印出买票窗口和票号System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);}}}}class  TicketDemo2{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);Thread t3 = new Thread(t);Thread t4 = new Thread(t);t1.start();t2.start();t3.start();t4.start();}}

运行结果:

通过运行结果,我们发现打印出了0-1-2等错误票号。出现了安全问题。

问题出现的原因:

        是因为多条语句在操作同一个线程共享数据时,一条语句只执行了一部分,还没执行完,另一条语句参与进来,导致了共享数据出错。

解决思路:

       在多条语句操作同一个共享数据时,只能让一个线程执行完毕,才能让其它线程参与进来。在执行过程中,其它线程不可以参与执行。

JAVA提供的多线程安全问题解决办法:

        java对于多线程的安全问题提供了专业的解决方式---同步。关键字:synchronized

实现方式1,同步代码块:

synchronized(对象)//对象可以是任意对象

{

需要同步的代码;

}

我们用同步代码块将上面你的卖票程序的Ticket类进行修改。修改后代码如下:

class Ticket implements Runnable{private  int tick = 1000;  //创建一个Object类对象obj作为同步代码块的对象Object obj = new Object();public void run(){while(true){synchronized(obj)//同步代码块{if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....sale:"+ tick--);}}}}}

运行结果:

        通过多次测试并观察运行结果,发现不会再打印出0-1-2等错误票号,多线程的安全问题得以解决。

        同步代码块的原理可以理解为,synchronized(对象)的这个“对象”中有一个默认的标识位,比如默认为1,在线程想要运行同步代码块中的内容时,先判断标识位是否为1,如果为1,则准备执行代码块中的内容,在执行之前,先把标识位的值改为0,然后开始执行同步代码块中的内容,执行完毕后,又先将标识位的值重新赋为1,最后释放同步锁。那么在执行期间,其它线程想要获取同步锁,当判断标识位的时候,发现标识位为0,那么获取失败。所以不能执行代码块中的内容。这就保证了一定时间段内只会有一个线程在执行同步代码块中的内容。其它线程参与不进来,这就杜绝了多个线程同时操作同一个共享数据。

实现方式2,同步函数:

        用synchronized修饰的方法是一个横跨整个方法体的同步代码块锁为方法调用所在的对象(this)如果该方法为静态方法,则锁为Class。需要注意的是,使用同步函数的时候必须明确函数内的代码都是需要同步的,如果将不需要同步的代码放在方法中进行了同步,可能会导致安全问题。比如我们将上面卖票程序通过同步函数进行同步,并只通过两个线程运行,代码如下:

class Ticket implements Runnable{private  int tick = 100;public synchronized void run(){while(true){if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);}}}}class  TicketDemo3{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();}}

但是运行结果却显示只有一个线程在执行。虽然是多个线程访问,但是执行结果却为单线程。

原因是,我们需要同步的代码只是下面这个部分:

if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....sale:"+ tick--);}

         而同步函数却同步了函数却同步了其它的代码,导致一个线程获取到锁之后一直执行,直到循环结束。因此,正确的做法是将需要同步的代码提取出来,重新定义为一个新的函数,并将这个新的函数同步,然后在需要执行的地方进行调用。代码如下:

class Ticket implements Runnable{private  int tick = 100;public  void run(){while(true){//调用show方法show();}}//将需要同步的代码定义为一个show函数并进行同步public synchronized void show(){if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);}}}class  TicketDemo3{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();}}

运行结果如下:

        同步代码块用的是“对象”锁,同步锁都是针对对象而言的,当synchronized直接修饰类的方法或者代码段时,那么此时的同步体就是方法所在类的实例,也即this对象。为了证明我们做如下实验,将上面卖票的例子分别通过两个线程执行,一个线程通过同步函数,一个线程通过同步代码块。

class Ticket implements Runnable{private  int tick = 100;Object obj = new Object();boolean flag = true;//定义一个标记用于控制线程执行顺序public  void run(){if (flag)//如果标记为真则执行同步代码块{while(true){synchronized(obj){if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....codesale : "+ tick--);}}}}elsewhile(true)show();}//将需要同步的代码定义为一个show函数并进行同步public synchronized void show(){if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....funcsale : "+ tick--);}}}class  TicketDemo3{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();//t1线程先运行,此时flag=true,则t1执行同步代码块。  //为了避免执行到这一步cpu执行权被主函数夺走,然后瞬间将后面代码执行完毕  //造成只有同步函数执行的情况。try{Thread.sleep(10);}catch(Exception e){}t.flag = false;//将flag标记修改为false让t2线程通过同步函数执行t2.start();}}

 

运行结果如下:

        通过观察运行结果发现出现0号票。我们已经加了同步,为什么还是会出现0号票呢?原因是两个线程用的不是同一个锁,既然同步函数用的同步锁是this,那么我们将同步代码块中的“对象”设置为this,这样t1t2用的额就是同一个锁了吧。为了进行验证,将同步代码块的代码进行如下改动:

synchronized(<span style="color:#ff0000;background-color: rgb(255, 255, 102);">this</span>){if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....codesale : "+ tick--);}}


 

修改后的执行结果如下:

 通过结果显示,已经不再出现0号票,证明了t1,t2用的是同一个锁,同时也证明了同步函数用的锁就是this

注意:如果同步函数被静态修饰,使用的锁不再是this是该方法所在类的字节码对象--->类名.class

我们通过下面的方法进行验证,首先将同步函数用static进行修饰

public static synchronized void show(){if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....funcsale : "+ tick--);}}

运行结果如下:

        运行结果显示同样出现了0号票,出现安全隐患。那么通过将同步代码块的“对象”更改为Ticket.class进行验证,如果0号票不再出现,证明t1,t2用的是同一个同步锁,也就是静态同步方法用的同步锁是类名.class。代码改动如下

synchronized(Ticket.class){if(tick>0){try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....codesale : "+ tick--);}}

更改后的运行结果如下:

结果显示没有出现0号票。原因是,静态中不可以定义this,当静态进内存时,内存中没有本类对象,但是有该类对应的字节码对象,即类名.class

多线程弊端:死锁

        死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问,一般是同步中嵌套同步。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性的访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。 

一个简单的死锁示例:

class Test implements Runnable{private boolean flag;Test(boolean flag){this.flag = flag;}public void run(){if(flag){while(true){synchronized(MyLock.locka){System.out.println(Thread.currentThread().getName()+"...if locka ");synchronized(MyLock.lockb){System.out.println(Thread.currentThread().getName()+"..if lockb");}}}}else{while(true){synchronized(MyLock.lockb){System.out.println(Thread.currentThread().getName()+"..else lockb");synchronized(MyLock.locka){System.out.println(Thread.currentThread().getName()+".....else locka");}}}}}}class MyLock{static Object locka = new Object();static Object lockb = new Object();}class  DeadLockTest{public static void main(String[] args) {Thread t1 = new Thread(new Test(true));Thread t2 = new Thread(new Test(false));t1.start();t2.start();}}

       造成上面这种运行结果的原因是,线程t1拿着MyLock.locka锁执行到System.out.println(Thread.currentThread().getName()+"...if locka ");后,想要获取MyLock.lockb锁,而同时线程t2拿着MyLock.lockb锁想要获取MyLock.locka锁,而这个时候a锁和b锁都已经被占用,那么线程t1和线程t2都发生阻塞,程序死锁。

 

------- java学习型技术博客、期待与您交流 ----------

原创粉丝点击