框架图
多线程
进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的控制单元。线程在控制着进程的执行。
一个进程中至少有一个线程。
如何在自定义的代码中,自定义一个线程呢?
创建线程的第一种方式:继承Thread类。
步骤:
1,定义类继承Thread。
2,复写Thread类中的run方法。
目的:用于存储线程要运行的代码。
3,调用线程的start方法,
该方法两个作用:启动线程,调用run方法。
1 class Demo extends Thread//继承Thread类 2 { 3 public void run()//复写run方法 4 { 5 for(int x=0; x<60; x++) 6 System.out.println("demo run----"+x); 7 } 8 } 9 class ThreadDemo 10 { 11 public static void main(String[] args) 12 { 13 Demo d = new Demo();//创建好一个线程对象。 14 d.start();//开启线程并执行该线程的run方法。 15 //d.run();//仅仅是对象调用方法。而线程创建了,并没有运行。 16 for(int x=0; x<60; x++) 17 System.out.println("Hello World!--"+x); 18 19 } 20 }
发现运行结果每一次都不同。因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。这就是多线程的一个特性:随机性-----谁抢到谁执行,至于执行多长,cpu说的算。
1 /*练习:创建两个线程,和主线程交替运行。 2 线程都有自己默认的名称。 3 Thread-编号 该编号从0开始。 4 static Thread currentThread():获取当前线程对象。 5 getName(): 获取线程名称。 6 设置线程名称:setName或者构造函数。 7 */ 8 class Test extends Thread 9 { 10 //private String name; 11 Test(String name) 12 { 13 //this.name = name; 14 super(name); 15 } 16 public void run() 17 { 18 for(int x=0; x<60; x++) 19 { 20 System.out.println((Thread.currentThread()==this)+"..."+this.getName()+" run..."+x); 21 } 22 } 23 24 } 25 26 27 class ThreadTest 28 { 29 public static void main(String[] args) 30 { 31 Test t1 = new Test("one---"); 32 Test t2 = new Test("two+++"); 33 t1.start(); 34 t2.start(); 35 for(int x=0; x<60; x++) 36 { 37 System.out.println("main....."+x); 38 } 39 } 40 }
创建线程的第二种方式:实现Runable接口。
步骤:
1,定义类实现Runnable接口
2,覆盖Runnable接口中的run方法。
将线程要运行的代码存放在该run方法中。
3,通过Thread类建立线程对象。
4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
自定义的run方法所属的对象是Runnable接口的子类对象,要让线程去指定指定对象的run方法。就必须明确该run方法所属对象。
5,调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
1 /* 2 需求:简单的卖票程序。 3 4个窗口同时买票。*/ 4 class Ticket implements Runnable//定义类实现Runnable接口 5 { 6 private int tick = 100; 7 public void run()//覆盖Runnable接口中的run方法 8 { 9 while(true) 10 { 11 if(tick>0) 12 { 13 System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); 14 } 15 } 16 } 17 } 18 class TicketDemo 19 { 20 public static void main(String[] args) 21 { 22 23 Ticket t = new Ticket(); 24 25 Thread t1 = new Thread(t);//通过Thread类建立线程对象,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数 26 Thread t2 = new Thread(t); 27 Thread t3 = new Thread(t); 28 Thread t4 = new Thread(t); 29 t1.start();//调用Thread类的start方法开启线程并调用Runnable接口子类的run方法 30 t2.start(); 31 t3.start(); 32 t4.start(); 33 34 35 } 36 }
实现方式和继承方式区别
子类如果已经继承了父类,就不能再继承Thread类,只能通过实现Runnable接口的方式来运行多线程的代码。避免了单继承的局限性,资源也可以被独立使用,推荐使用实现方式。
实现方式和继承方式的线程代码存放的位置也不一样。继承Thread方法,线程方法存放在Thread及其子类的run方法中;实现Runnable方法,线程方法存在于接口的子类的run方法中,即新建类中。
run方法内部出现了异常,只能在内部解决,不能抛出,Runnable中的run没有异常处理方法。
线程状态示例图
冻结。线程暂停,但没有结束。sleep(),暂停指定time时间后,自动运行;wait()等待,不传入参数不会运行;notify(),唤醒,线程继续运行。冻结状态放弃了执行资格,时间到了,或被唤醒后,进入临时状态,等待获得运行权。
消亡。run方法运行完毕后,自动消亡,或者stop(),运行时让线程终止。
临时状态又称阻塞状态。CPU一次只能运行一个线程,其他线程都在临时状态,在等待CPU的执行权;也就是说start后线程不一定马上运行,此时具备运行资格(内存中拥有空间且随时可以运行),但没有运行权(获取资源被CPU执行)。
运行,既有运行资格,又有运行权的状态。
多线程运行时的安全问题
上面卖票的代码示例有可能会实现一张票卖多次,还可能出现0号票和负数票的可能。发生这个问题的原因是,所有进程是同步运行的,可能会同一张票被买了多次。这种情况不是时时都会发生,但肯定是有可能会发生的。为了查看这种情况,调用Thread的sleep()方法。
1 class Ticket implements Runnable 2 { 3 private int tick = 100; 4 public void run() 5 { 6 while(true) 7 { 8 if(tick>0) 9 { 10 try{Thread.sleep(10);}catch(InterruptedException e){e.printStackTrace();} 11 System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); 12 } 13 } 14 } 15 } 16 class TicketDemo 17 { 18 public static void main(String[] args) 19 { 20 21 Ticket t = new Ticket(); 22 23 Thread t1 = new Thread(t); 24 Thread t2 = new Thread(t); 25 Thread t3 = new Thread(t); 26 Thread t4 = new Thread(t); 27 t1.start(); 28 t2.start(); 29 t3.start(); 30 t4.start(); 31 } 32 }
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。Java对于多线程的安全问题提供了专业的解决方式,就是同步代码块和同步函数。
同步代码块
synchronized(对象)
{
需要被同步的代码
}
对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。比如火车上的卫生间例子。
如何判断需要被同步的代码:
1. 明确哪些代码是多线程运行代码。
2. 明确共享数据。
3. 明确多线程运行代码中哪些语句是操作共享数据的。同步指定的语句,即操作共享数据的语句。
同步的前提:
1,必须要有两个或者两个以上的线程。
2,必须是多个线程使用同一个锁。
同步函数
格式:在函数上加上synchronized修饰符即可。
同步函数使用的锁是this,函数需要被对象调用,函数都有一个所属对象的引用,即this。
注意:同步函数被静态修饰后,使用的锁不再是this,使用的锁是该方法所在类的字节码文件对象,即类名.class。因为静态方法中没有this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象,即类名.class,该对象的类型是Class,此文件在内存中是唯一的,因为字节码文件是一次加载,不再重复。
同步的好处和弊端:解决了多线程的安全问题;多个线程需要判断锁,较为消耗资源。
死锁
同步中嵌套同步,而锁不同,有可能导致死锁,开发时应尽量避免死锁。
1 /*死锁示例*/ 2 class Test implements Runnable 3 { 4 private boolean flag; 5 Test(boolean flag) 6 { 7 this.flag = flag; 8 } 9 10 public void run() 11 { 12 if(flag) 13 { 14 while(true) 15 { 16 synchronized(MyLock.locka) 17 { 18 System.out.println(Thread.currentThread().getName()+"...if locka"); 19 synchronized(MyLock.lockb) 20 { 21 System.out.println(Thread.currentThread().getName()+"...if lockb"); 22 } 23 } 24 } 25 } 26 else 27 { 28 while(true) 29 { 30 synchronized(MyLock.lockb) 31 { 32 System.out.println(Thread.currentThread().getName()+".....else lockb"); 33 synchronized(MyLock.locka) 34 { 35 System.out.println(Thread.currentThread().getName()+".....else locka"); 36 } 37 } 38 } 39 } 40 } 41 } 42 43 class MyLock 44 { 45 static Object locka = new Object(); 46 static Object lockb = new Object(); 47 } 48 class DeadLockTest 49 { 50 public static void main(String[] args) 51 { 52 Thread t1 = new Thread(new Test(true)); 53 Thread t2 = new Thread(new Test(false)); 54 t1.start(); 55 t2.start(); 56 } 57 }
线程间通信
就是多个线程在操作同一个资源,但是操作的动作不同。
wait();//wait(),sleep()区别wait()释放cpu执行权,释放锁。sleep()释放cpu执行权,不释放锁。
notify();
notifyAll();
都使用在同步中,因为要对持有监视器(锁)的线程操作,只有同步才具有锁。
都要定义Object类中,因为这些方法在操作同步中线程时,都必须要标识它们所操作线程持有的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义Object类中。
生产者消费者代码示例
1 class ProducerConsumerDemo 2 { 3 public static void main(String[] args) 4 { 5 Resource r = new Resource(); 6 7 Producer pro = new Producer(r); 8 Consumer con = new Consumer(r); 9 10 Thread t1 = new Thread(pro); 11 Thread t2 = new Thread(pro); 12 Thread t3 = new Thread(con); 13 Thread t4 = new Thread(con); 14 15 t1.start(); 16 t2.start(); 17 t3.start(); 18 t4.start(); 19 20 //new Thread(new Producer(r)).start();匿名对象写法。 21 //new Thread(new Producer(r)).start(); 22 //new Thread(new Consumer(r)).start(); 23 //new Thread(new Consumer(r)).start(); 24 25 } 26 } 27 class Resource 28 { 29 private String name; 30 private int count = 1; 31 private boolean flag = false; 32 33 public synchronized void set(String name) 34 { 35 while(flag)//对于多个生产者和消费者线程,要定义while判断标记,为的是让被唤醒的线程再一次判断标记。 36 try{this.wait();}catch(InterruptedException e){} 37 this.name = name+"--"+count++; 38 39 System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name); 40 flag = true; 41 this.notifyAll();//需要唤醒对方线程定义notifyAll,因为只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。 42 } 43 public synchronized void out() 44 { 45 while(!flag) 46 try{wait();}catch(InterruptedException e){} 47 System.out.println(Thread.currentThread().getName()+"...消费者.........."+this.name); 48 flag = false; 49 this.notifyAll(); 50 } 51 } 52 class Producer implements Runnable 53 { 54 private Resource res; 55 56 Producer(Resource res) 57 { 58 this.res = res; 59 } 60 public void run() 61 { 62 while(true) 63 { 64 res.set("+商品+"); 65 } 66 } 67 } 68 class Consumer implements Runnable 69 { 70 private Resource res; 71 72 Consumer(Resource res) 73 { 74 this.res = res; 75 } 76 public void run() 77 { 78 while(true) 79 { 80 res.out(); 81 } 82 } 83 }
JDK1.5 中提供了多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。
将Object中的wait,notify notifyAll,替换成了Condition对象。
该对象可以Lock锁进行获取。
该示例中,实现了本方只唤醒对方操作。
Lock:替代了Synchronized
lock
unlock
newCondition()
Condition:替代了Object中 wait notify notifyAll
await();
signal();
signalAll();
生产者消费者代码1.5示例
1 import java.util.concurrent.locks.*; 2 class ProducerConsumerDemo2 3 { 4 public static void main(String[] args) 5 { 6 Resource r = new Resource(); 7 8 Producer pro = new Producer(r); 9 Consumer con = new Consumer(r); 10 11 Thread t1 = new Thread(pro); 12 Thread t2 = new Thread(pro); 13 Thread t3 = new Thread(con); 14 Thread t4 = new Thread(con); 15 16 t1.start(); 17 t2.start(); 18 t3.start(); 19 t4.start(); 20 21 } 22 } 23 class Resource 24 { 25 private String name; 26 private int count = 1; 27 private boolean flag = false; 28 29 private Lock lock = new ReentrantLock();//创建一个lock对象 30 31 private Condition condition_pro = lock.newCondition();//返回绑定到此lock实例的Condition实例 32 private Condition condition_con = lock.newCondition(); 33 34 35 36 public void set(String name)throws InterruptedException 37 { 38 lock.lock();//获取锁。 39 try 40 { 41 while(flag) 42 condition_pro.await(); 43 this.name = name+"--"+count++; 44 System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name); 45 flag = true; 46 condition_con.signal(); 47 } 48 finally 49 { 50 lock.unlock();//释放锁的动作一定要执行。 51 } 52 } 53 public void out()throws InterruptedException 54 { 55 lock.lock(); 56 try 57 { 58 while(!flag) 59 condition_con.await(); 60 System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name); 61 flag = false; 62 condition_pro.signal(); 63 } 64 finally 65 { 66 lock.unlock(); 67 } 68 69 } 70 } 71 class Producer implements Runnable 72 { 73 private Resource res; 74 75 Producer(Resource res) 76 { 77 this.res = res; 78 } 79 public void run() 80 { 81 while(true) 82 { 83 try 84 { 85 res.set("+商品+"); 86 } 87 catch (InterruptedException e) 88 { 89 } 90 91 } 92 } 93 } 94 95 class Consumer implements Runnable 96 { 97 private Resource res; 98 99 Consumer(Resource res) 100 { 101 this.res = res; 102 } 103 public void run() 104 { 105 while(true) 106 { 107 try 108 { 109 res.out(); 110 } 111 catch (InterruptedException e) 112 { 113 } 114 } 115 } 116 }
优势在于,一个锁里可以绑定多个Condition对象。以前同步里面多个线程拥有一个锁,绑定一个对象,等待唤醒操作也只能由这个对象决定;现在锁可以绑定多个对象,进而有多个等待唤醒操作,操作更加灵活,只需要区分Condition对象即可。最大的优点是可以做到本方只唤醒对方的操作。可以在嵌套中使用,不会出现死锁的情况。
停止线程
stop方法已经过时不再使用。
1.定义循环结束标记
因为线程运行代码一般都是循环,只要控制了循环即可停止线程。
2.使用interrupt(中断)方法。
该方法是清除线程的冻结状态,使线程回到运行状态中来。
join()
当A线程执行到了B线程的join()方法时,A就会等待。等B线程都执行完,A才会执行。join()可以用来临时加入线程执行。
setPriority(int num)
设置线程优先级数。
static int MAX_PRIORITY
线程可以具有的最高优先级。
static int MIN_PRIORITY
线程可以具有的最低优先级。
static int NORM_PRIORITY
分配给线程的默认优先级。
setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
toString()
返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
yield()
暂停当前正在执行的线程对象,并执行其他线程。
实际开发中一般多线程写法
1 class ThreadTest 2 { 3 public static void main(String[] args) 4 { 5 6 new Thread() 7 { 8 public void run() 9 { 10 for(int x=0; x<100; x++) 11 { 12 System.out.println(Thread.currentThread().getName()+"....."+x); 13 } 14 } 15 }.start(); 16 17 18 for(int x=0; x<100; x++) 19 { 20 System.out.println(Thread.currentThread().getName()+"....."+x); 21 } 22 23 Runnable r = new Runnable() 24 { 25 public void run() 26 { 27 for(int x=0; x<100; x++) 28 { 29 System.out.println(Thread.currentThread().getName()+"....."+x); 30 } 31 } 32 }; 33 new Thread(r).start(); 34 /* 35 new Thread(new Runnable() 36 { 37 public void run() 38 { 39 for(int x=0; x<100; x++) 40 { 41 System.out.println(Thread.currentThread().getName()+"....."+x); 42 } 43 } 44 }).start(); 45 */ 46 } 47 }