Java基础13--多线程

来源:互联网 发布:23种设计模式java 编辑:程序博客网 时间:2024/06/01 20:18

13-1,线程的状态

1,Thread的stop()方法能强制结束线程。

2,两个概念(非专业术语):

CPU的执行资格:可以被CPU处理,在处理队列中排队;

CPU的执行权:正在被CPU处理。

线程的执行过程:

线程先被创建,然后用start方法运行,此时具备执行资格和执行权,运行完成后消亡。在运行时线程可以临时冻结,此时释放执行资格和执行权,由于CPU在同一时间只能运行一个线程,所以除了正在执行的线程,其他线程都处于临时阻塞状态,此时具备执行资格但不具备执行权。

3,线程状态图示:


线程的状态:被创建、运行冻结、消亡、临时阻塞状态,横线为重点掌握。

 

13-2,创建线程的第二种方式:

1,第二种方式:实现Runnable接口。

步骤:

(1)定义类实现Runnable接口。

(2)覆盖接口中的run方法,将线程的任务代码封装到run方法中。

(3)通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。为什么呢?因为线程的任务都封装在Runnable接口的子类对象的run方法中,所以要在对象创建时就必须明确要运行的任务。

(4)调用线程对象的start方法开启线程。

2,当一个类已经有父类时,就不能通过Thread类来实现多线程了,只能通过实现Runnable接口来实现多线程。

3,实现Runnable接口的代码示例:

//准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行,通过接口的形式来完成。

class Demo implements Runnable {public void run() { //重写run方法show();}public void show() {for(int x=0;x<20;x++) {System.out.println(Thread.currentThread.getName()+ "..." + x);}}}class ThreadDemo {public static void main(String[] args) {Demo d = new Demo();/*Thread类创建线程对象,将Runnable的子类Demo的对象作为Thread类的构造函数的参数进行传递。*/Thread t1 = new Thread(d);Thread t2 = new Thread(d);//调用线程对象的start方法开启线程。t1.start();t2.start();}}

 

13-3,多线程-第二种实现方式细节

为什么把Runnable的子类对象作为参数传递给Thread类的构造函数,执行的就是子类中的run方法呢?

下面通过模拟Thread类的实现来说明。看下列代码:

class Thread {private Runnable r;Thread() {}Thread(Runnable r) {this.r = r;}public void run() {if(r != null) {r.run();}}public void start() {run();}}class ThreadImpl implements Runnable {public void run() {System.out.println("runnable run");}}class Demo {public static void main(String[] args) {ThreadImpl i = new ThreadImpl();Thread t = new Thread(i);t.start();}}

Thread类中有一个Runnable的引用r,当把子类对象传入时,这个r就指向子类对象,run方法中判断如果传入了参数,就执行子类中的run方法,否则什么也不做。

 

13-4,第二种实现方式的好处

好处:

(1)将线程的任务从线程的子类中分离出来,进行了单独的封装。

按照面向对象的思想将任务封装成对象。

(2)避免了Java单继承的局限性。

所以创建线程的第二种方式较为常用。Runnable的出现仅仅是将线程的任务进行了对象的封装。

 

13-5,多线程-练习-买票

用多线程模拟:游乐园有四个售票口,一共买一百张票。

分析:每个售票口都是一个单独的线程,随机的售票,一个售票口的关闭不会影响其他窗口售票。

实现:

class Ticket implements Runnable {private int num = 100;//票的总数public void run() {while(true) {if(num > 0) { //票数不能是负数System.out.println(Thread.currentThread().getName()+ "..sale.." + num--);}}}}class TicketDemo {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();}}

 

13-6,线程安全问题现象

以13-5中的例子来说。

class Ticket implements Runnable {private int num = 100;//票的总数public void run() {while(true) {if(num > 0) { //票数不能是负数System.out.println(Thread.currentThread().getName()+ "..sale.." + num--);}}}}

这个例子中,开启了四个线程,Thread-0到Thread-3,此时若num=1,run方法执行,if(num>0)成立,但Thread-0线程读完if语句后没有执行Sop打印,CPU切到别的线程执行,这时比如切到了Thread-1,Thread-1线程也只读了if语句,因为这时num还等于1,所以if成立,这时CPU又切到了其他的Thread线程,Thread-2与Thread-3也是一样。然后这些线程都只有执行资格但不具备执行权,CPU又切回Thread-0线程,然后执行Thread-0的打印输出了,打印Thread-0..sale..1,然后num--,这时num=0,由于Thread-1的if已经执行,所以不再判断,直接执行Thread-1的Sop输出,打印Thread-1..sale..0,Thread-2和Thread-3同样,打印出Thread-2..sale..-1和Thread-3..sale..-2,这样就出现了不期望出现的结果,产生了安全问题。

 

13-7,线程安全问题产生的原因

1,产生原因:上例中由于num是在堆中的共享数据,当执行完if而没有执行sop,CPU切到别的线程,再返回该线程,num的值就可能产生变化,而此时将不再执行if,直接执行sop,但此时的结果可能不是预想的结果了。

如果执行完if马上执行sop就不会出现这个问题了。

2,线程安全问题产生的原因:

(1)多个线程在操作共享的数据。

(2)操作共享数据的线程代码有多条。

当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致安全问题的产生。

 

13-8,同步代码块—解决安全问题

1,解决思路:

就是将多条操作共享数据的线程代码封装起来,当有现成在执行这些代码的时候,其他线程是不可以参与运算的,必须要当前线程把这些代码都执行完毕,其他线程才可以参与运算。

2,在Java中,用同步代码块就可以解决这个问题。

同步代码块的格式:

synchronized(对象) {

    需要被同步的代码...

}

示例:

class Ticket implements Runnable {private int num = 100;Object obj = new Object();public void run() {while(true) {synchronized(obj) {if(num > 0) {try {Thread.sleep(10);}catch(InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "...sale..." + num--);}}}}}

13-9 ,多线程-同步的好处和弊端

1,同步代码块解决安全问题的原理

synchronized(obj) { //obj是锁,可以理解为1表示开锁,0表示关锁if(num > 0) {try {Thread.sleep(10);}catch(InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "...sale..." + num--);}}

当线程1到同步代码块时,obj为1,即此时线程1可以进入执行代码,而线程1一进入,锁就置为0,关闭锁,就算线程1在里面sleep,别的线程也无法进入。一旦线程1执行完,锁再从0置为1,开锁,这时别的线程就可以进入了,其他线程的原理也是如此。

2,同步的好处:解决了线程的安全问题。

同步的弊端:相对降低了效率,因为同步的线程都会判断同步锁。

 

13-10,多线程-同步的前提

前提:同步中必须有多个线程并且使用同一个锁。

若将13-8中的代码Objectobj = new Object();放在while(true)的上面,这样每开启一次线程,每个线程的run都会给自己的线程创建一个锁,这样四个锁互不影响,就不能实现四个线程的同步了。

 

13-11,多线程-同步函数

用一个例子来说明代码中哪里存在安全隐患。

/*需求:有两个储户,去银行存钱,每人每次存100,共存三次。银行有金库,用sum来记录存入的钱数,两个人在同一个银行存,存的钱数都累加到sum中。*/class Bank {private int sum;/*这里可能出现安全隐患,在执行完sum = sum + num后,若CPU切走,输出可能出现问题*/public synchronized void add(int num) { //这就是同步函数,给函数加上synchronized修饰sum = sum + num;try {Thread.sleep(10);}catch(InterruptedException e) {}System.out.println("sum = " + sum);}}class Cus implements Runnable {//创建Bank对象,调用add方法,不能在run中创建,不然就相当于两个人分别去两个不同的银行存钱。private Bank b = new Bank();public void run() {for(int x=0;x<3;x++) {b.add(100); //只有一条语句,这里一般不出现问题}}}class BankDemo {public static void main(String[] args) {Cus c = new Cus();Thread t1 = new Thread(c);Thread t2 = new Thread(c);t1.start();t2.start();}}

 

13-12,多线程-验证同步函数的锁

1,以程序来说明,本例中一个线程用同步代码块,另一个用同步函数,若两个线程使用同一个锁,则结果正确,否则结果错误。

class Ticket implements Runnable {private int num = 100;private Object obj = new Object();//定义标记,若为true执行同步代码块,如果false执行同步函数boolean flag = true;public void run() {if(flag) {while(true) {synchronized(obj) { //这里如果改为this则结果正确if(num > 0) {try {Thread.sleep(10);}catch(InterruptedException e) {}System.out.println(Thread.currentThread().getName ()+"obj"+num--);}}}} else {while(true) {this.show(); //调用同步函数}}}public synchronized void show() {if(num > 0) {try {Thread.sleep(10);}catch(InterruptedException e) {}System.out.println(Thread.currentThread().getName()+"show"+num--);}}}class SynFunctionLockDemo {public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();//让线程1冻结一下,不然CPU执行太快会直接按false执行下面的线程2try {Thread.sleep(10);}catch(InterruptedException e) {}t.flag = false;t2.start();}}

该程序说明了同步代码块和同步函数使用的不是同一个锁。

2,同步函数使用的锁是this。

同步函数和同步代码块的区别:

同步函数的锁是固定的this。

同步代码块的锁是任意的对象。

建议使用同步代码块,同步函数是同步代码块的简写,简写会方便一些,但是有弊端。

 

13-13,验证静态同步函数的锁

1,静态的同步函数使用的锁是:该函数所属字节码文件对象。

可以通过getClass()方式获取,也可以使用 当前类名.class 获取。

2,若把13-12中的代码的publicsynchronizedvoid show()改为public static synchronizedvoid show()则此方法为静态的,静态函数中没有this,其实静态方法随着类的加载而被加载进静态方法区后,也带有一个对象这个对象就是该方法所属类的字节码文件,所以在同步代码块的对象的位置上写上this.getClass()或Ticket.class就可以使结果正确。

 

13-14,单例模式涉及的多线程问题

饿汉式:

class Single {private static final Single s = new Single();private Single(){}public static Single getInstance() {return s; //操作共享数据的只有一句话,不存在安全问题}}

懒汉式:


13-15,多线程-死锁示例

死锁,常见的情况之一:同步的嵌套,以代码说明:

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 {//创建锁对象,静态对象直接类名调用public static final Object locka = new Object();public static final Object lockb = new Object();}class DeadLockTest {public static void main(String[] args) {//new两个Test对象,为了让t1进程执行if中的语句,让t2进程执行else中的语句。Test a = new Test(true);Test b = new Test(false);Thread t1 = new Thread(a);Thread t2 = new Thread(b);t1.start();t2.start();}}

说明,可能会出现的情况:

对象a初始化为true,所以线程t1执行if中的语句,这时线程t1持有了locka锁,并输出了if locka,这时CPU切走线程,线程t2开始执行,因为b的初始化为false,所以t2执行else中的语句,这时t2持有了lockb锁,并输出了elselockb,这时如果t2继续向下执行,将会索取locka锁,而这时locka锁正被t1拿着,从而无法获得,CPU转而执行t1,t1继续执行将会索取lockb锁,而lockb锁正被t2拿着,这样t1和t2都无法继续执行,就挂在这里,就行成了死锁。

若t2切走去执行t1,t1将索取lockb,这时lockb被t2拿着无法索取,CPU再执行t2,t2将索取locka,但locka被t1拿着,t1和t2都无法继续执行,也形成了死锁。


0 0
原创粉丝点击