JavaSE基础---多线程
进程:正在进行的程序。其实就是一个应用程序运行时的内存分配空间。
线程:进程中一个程序执行控制单元,一条执行路径。进程负责的事应用程序的空间的标识,线程负责的事应用程序的执行顺序。
进程和线程的关系:一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序,每个线程在栈区中都有自己的执行空间,自己的方法区、和变量。
JVM启动时,首先有一个主线程,负责程序的执行,调用的是main函数。主线程执行的代码都在main方法中。
当产生垃圾时,收垃圾的动作,是不需要主线程来完成的,应为这样,会出现主线程中的代码执行停止,去运行垃圾会收器代码,效率较低,所以由一个单独的线程来负责垃圾回收。
一、创建多线程程序的两种方法:Thread类和Runnable接口
(一)通过继承Thread类来完成
1、步骤:
(1)定义类继承Thread类;
(2)复写run方法,将要让线程运行的代码都储存到run方法中
(3)通过创建Thread类的子类对象,创建线程对象
(4)调用线程的start方法,开启线程,并自动执行run方法,注意:start()方法会执行两条命令,1、开启线程,2、执行run方法
2、线程的状态:
(1)被创建:start();
(2)运行:具备执行资格,同时具备执行权。
(3)冻结:sleep(time),wait()---notify()唤醒;线程释放执行权,同时释放执行资格;
(4)零时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权
(5)消亡:stop()
(二)通过实现Runnable接口来完成(如果一个类已经继承了其他类,就不能继承Thread类了,Java的单继承局限性。于是只能对该类进行额外的功能扩展)
1、步骤:
(1)定义类实现Runnbale接口;
(2)覆盖接口中的run方法(封装线程要执行的代码);
(3)通过Thread类创建线程对象;
(4)将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数;(明确线程要执行的代码run方法)
(5)调用Thread对象的start()方法,开启线程,并执行Runnable接口的子类对象中的run()方法。
2、Runnable接口
(1)避免了Java单继承的局限性
(2)Thread类描述时,内部定义的run方法,也来自于Runnable接口
(3)Runnable接口将线程要执行的任务封装成了对象
(三)案例
1、买票
(1)通过继承Thread类
1 class Ticket extends Thread 2 { 3 private static int num = 100;//出售票的总数,如果不设置静态,每创建一个对象,就会有属于自己的100张票,最终会出售400张票,所以设置为静态,让他成为所有对象共享的数据。 4 public void run() 5 { 6 while(true) 7 { 8 if(num>0) 9 { 10 System.out.println(Thread.currentThread().getName()+"...sale..."+num--); 11 } 12 } 13 } 14 } 15 16 class TicketDemo 17 { 18 public static void main(String[] args) 19 { 20 Ticket t1 = new Ticket(); 21 Ticket t2 = new Ticket(); 22 Ticket t3 = new Ticket(); 23 Ticket t4 = new Ticket(); 24 25 t1.start(); 26 t2.start(); 27 t3.start(); 28 t4.start(); 29 } 30 }
(2)通过实现Runnable接口
1 class Ticket implements Runnable 2 { 3 private int num = 100;//出售票的总数 4 public void run() 5 { 6 while(true) 7 { 8 if(num>0) 9 { 10 System.out.println(Thread.currentThread().getName()+"...sale..."+num--); 11 } 12 } 13 } 14 } 15 16 class TicketDemo 17 { 18 public static void main(String[] args) 19 { 20 Ticket t = new Ticket(); 21 Thread t1 = new Thread(t); 22 Thread t2 = new Thread(t); 23 Thread t3 = new Thread(t); 24 Thread t4 = new Thread(t); 25 26 t1.start(); 27 t2.start(); 28 t3.start(); 29 t4.start(); 30 } 31 }
二、多线程的安全问题
当一个线程在执行操作共享数据的多条带没带过程中,其他线程参与了运算,会导致线程安全问题的产生。
1、出现安全问题的两个前提
(1)多个线程在操作共享数据
(2)操作共享数据的线程代码有多条
2、解决方案
只要让某一线程在执行操作共享数据的多条代码时,让其他程序不能执行数据的操作就可以了。即同步
三、同步
1、定义同步的两个前提
(1)必须要有两个或两个以上的线程,才需要同步
(2)多个线程必须保证使用的是同一个锁
2、同步的两种表现形式
(1)同步代码块:
格式:
synchronized(对象)//可以是任意对象,该对象其实就是锁 { //需要被同步的代码 }
案例:
a、上边买票的案例优化
1 class Ticket implements Runnable 2 { 3 private int num = 100;//出售票的总数 4 Object obj = new Object(); 5 public void run() 6 { 7 while(true) 8 { 9 synchronized(obj) 10 { 11 if(num>0) 12 { 13 try { 14 Thread.sleep(10); 15 } catch (InterruptedException e) { 16 17 } 18 System.out.println(Thread.currentThread().getName()+"...sale..."+num--); 19 } 20 } 21 } 22 } 23 } 24 25 class TicketDemo 26 { 27 public static void main(String[] args) 28 { 29 Ticket t = new Ticket(); 30 Thread t1 = new Thread(t); 31 Thread t2 = new Thread(t); 32 Thread t3 = new Thread(t); 33 Thread t4 = new Thread(t); 34 35 t1.start(); 36 t2.start(); 37 t3.start(); 38 t4.start(); 39 } 40 }
b、一张银行卡,两个人每人每次到银行存100,存三次
1 class Bank 2 { 3 private int sum; 4 private Object obj = new Object(); 5 public void add(int num) 6 { 7 synchronized(obj)//注意:不能再此处直接new Object(),这样的话,每次创建一个对象,就会new一个新的Object,用的就不是同一个锁了, 8 { 9 sum = sum+num; 10 try{Thread.sleep(10);}catch (InterruptedException e){} 11 System.out.println(Thread.currentThread().getName()+"...sum="+sum); 12 } 13 } 14 } 15 16 class Cus implements Runnable 17 { 18 private Bank b = new Bank(); 19 public void run() 20 { 21 for (int x=3; x>0; x--) { 22 b.add(100); 23 } 24 } 25 } 26 27 class BankDemo 28 { 29 public static void main(String[] args) 30 { 31 Cus c = new Cus(); 32 Thread t1 = new Thread(c); 33 Thread t2 = new Thread(c); 34 35 t1.start(); 36 t2.start(); 37 38 } 39 }
(2)同步函数:就是将同步关键字定义在函数上,让函数具备同步性。
同步函数锁使用的锁就是this
当同步函数被static修饰时,静态函数的调用不需要对象,但是静态函数所属类的字节码文件在加载进内存时,这个字节码文件就被封装成了对象。所以此时同步函数所使用的锁就是该类的字节码文件对象。(类名.class)
案例:上边银行存款的例子、
1 class Bank 2 { 3 private int sum; 4 private Object obj = new Object(); 5 public synchronized void add(int num) 6 { 7 //synchronized(obj)//注意:不能再此处直接new Object(),这样的话,每次创建一个对象,就会new一个新的Object,用的就不是同一个锁了, 8 //{ 9 sum = sum+num; 10 try{Thread.sleep(10);}catch (InterruptedException e){} 11 System.out.println(Thread.currentThread().getName()+"...sum="+sum); 12 //} 13 } 14 } 15 16 class Cus implements Runnable 17 { 18 private Bank b = new Bank(); 19 public void run() 20 { 21 for (int x=3; x>0; x--) { 22 b.add(100); 23 } 24 } 25 } 26 27 class BankDemo 28 { 29 public static void main(String[] args) 30 { 31 Cus c = new Cus(); 32 Thread t1 = new Thread(c); 33 Thread t2 = new Thread(c); 34 35 t1.start(); 36 t2.start(); 37 38 } 39 }
2、同步代码块和同步函数的区别
(1)同步代码块使用的锁可以是任意对象。
同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。
3、用同步代码块还是同步函数
在一个类中只有一个同步时,可以使用同步函数,但是如果有多个同步,就必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些。
4、使用同步的好处和弊端
(1)好处:解决了线程的安全问题
(2)弊端:相对降低性能,因为判断锁需要消耗资源,产生了死锁。
死锁:常见情景之一:同步的嵌套
1 class Test implements Runnable 2 { 3 private boolean flag; 4 Test(boolean flag) 5 { 6 this.flag = flag; 7 } 8 9 public void run() 10 { 11 12 if(flag) 13 { 14 while(true) 15 synchronized(MyLock.locka) 16 { 17 System.out.println(Thread.currentThread().getName()+"..if locka...."); 18 synchronized(MyLock.lockb) { 19 20 System.out.println(Thread.currentThread().getName()+"..if lockb...."); 21 } 22 } 23 } 24 else 25 { 26 while(true) 27 synchronized(MyLock.lockb) 28 { 29 System.out.println(Thread.currentThread().getName()+"..else lockb...."); 30 synchronized(MyLock.locka) 31 { 32 System.out.println(Thread.currentThread().getName()+"..else locka...."); 33 } 34 } 35 } 36 37 } 38 39 } 40 41 class MyLock 42 { 43 public static final Object locka = new Object(); 44 public static final Object lockb = new Object(); 45 } 46 47 48 49 50 class DeadLockTest 51 { 52 public static void main(String[] args) 53 { 54 Test a = new Test(true); 55 Test b = new Test(false); 56 57 Thread t1 = new Thread(a); 58 Thread t2 = new Thread(b); 59 t1.start(); 60 t2.start(); 61 } 62 }
四、单例模式涉及的多线程问题
1、饿汉式
1 class Single 2 { 3 private static finally Single s = new Single();//静态方法访问的内容必须是静态的。 4 private Single(){} 5 public static Single getInstance()//因为单例不能在其他类中创建单例类对象,所以getInstance方法必须是静态的,可以直接被Single类调用。 6 { 7 return s; 8 } 9 }
2、懒汉式(单例模式的延迟加载形式)
当多线程访问懒汉式时,懒汉式的getInstance方法中对共享数据s进行了多条语句的操作,所以容易出现线程安全问题。为了解决该问题,加入同步机制,但是每次调用懒汉式对象,都会判断synchronized锁,降低了效率。于是通过双重判断的形式解决效率问题。
1 class Single 2 { 3 private static Single s = null; 4 private Single(){} 5 public static Single getInstance(){ 6 if(s==null) 7 { 8 synchronized(Single.class)//此处的锁是懒汉式类字节码文件对象 9 { 10 if(s==null) 11 s = new Single(); 12 } 13 } 14 return s; 15 } 16 }
五、多线程
1、线程间通讯
多个线程在操作同一个资源,但是操作的动作却不一样。
思路:(1)将资源封装成对象;
(2)将不同线程执行的任务(其实就是run方法)也封装在不同的对象中。
多生产者多消费者问题
1 /* 2 * 资源对象 3 */ 4 class Resource 5 { 6 private String name; 7 private int count =1; 8 private Boolean flag = false; 9 public synchronized void set(String name) 10 { 11 while(flag) 12 { 13 try{this.wait();}catch(InterruptedException e) {} 14 } 15 this.name = name+this.count; 16 System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name); 17 count++; 18 flag = true; 19 notifyAll();//t0,t1的锁是pro对象,t2,t3的锁是con对象,这里的notifyAll()只是唤醒以Pro对象为锁的线程 20 } 21 public synchronized void out() 22 { 23 while(!flag) 24 { 25 try{this.wait();}catch(InterruptedException e) {} 26 } 27 System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name); 28 flag = false; 29 notifyAll(); 30 } 31 } 32 33 /* 34 * 生产者对象 35 */ 36 class Producer implements Runnable 37 { 38 private Resource r; 39 Producer(Resource r) 40 { 41 this.r = r; 42 } 43 public void run() 44 { 45 while(true) 46 { 47 r.set("烤鸭"); 48 } 49 } 50 } 51 52 class Consumer implements Runnable 53 { 54 private Resource r; 55 Consumer(Resource r) 56 { 57 this.r = r; 58 } 59 public void run() 60 { 61 while(true) 62 { 63 r.out(); 64 } 65 } 66 } 67 68 /* 69 * 消费者对象 70 */ 71 class ProducerConsumerDemo 72 { 73 public static void main(String[] args) 74 { 75 Resource r = new Resource(); 76 Producer pro = new Producer(r); 77 Consumer con = new Consumer(r); 78 Thread t0 = new Thread(pro); 79 Thread t1 = new Thread(pro); 80 Thread t2 = new Thread(con); 81 Thread t3 = new Thread(con); 82 t0.start(); 83 t1.start(); 84 t2.start(); 85 t3.start(); 86 } 87 }
相关知识:
等待唤醒机制
wait(); 将同步中的线程处于冻结状态。释放了执行权,释放了执行资格。同时将线程对象存储到线程池中。
notify(); 唤醒线程池中某一个等待的线程。如果A锁上的线程被wait了,那么此时该线程就处于A锁的线程池中,只能由A锁的notify()唤醒。
notifyAll(); 唤醒线程池中所有线程。
以上三个方法都被定义在Object类中,是因为这些方法存在于同步中,调用时都必须要标识所属的同步的锁,而锁可以是任意对象,所以任意对象都可以使用的方法一定定义在Object类中。
wait和sleep的区别:
(1)wait可以指定等待时间也可以不指定时。不指定时间,只能由对应的notify()或者notifyAll()来唤醒
sleep必须指定时间,时间到了就自动从冻结状态转成运行状态(临时阻塞状态)
(2)wait线程会释放执行权,而且线程会释放锁
sleep线程会释放执行权,但不是不释放锁
线程的停止:
(1)通过stop方法停止,但是该方式已过时
(2)让线程运行的代码结束,也就是结束run方法。一般run方法里一定有循环,所以只要结束循环即可。a、定义循环的结束标记;b、如果线程处于冻结状态,是不能督导标记的,这时就需要通过Thread类的interrupt方法,将其冻结状态强制清除,让线程恢复到具备执行资格的状态,这时该线程就可以读到结束标记,并结束线程。
java.lang.Thread类相关方法:
interrupt() 中断线程,其实是将线程从冻结状态强制恢复到运行状态中,让线程具备CPU的之赐你个资格。
setPriority(int newPriority) 更改线程的优先级
getPriority(); 返回线程的优先级
tiString() 返回该线程的字符串表示形式,包括线程名称、优先级和线程组
Thread.yield() 暂停当前正在执行的线程对象,并执行其他线程
setDaemon(true) 将该线程标记为守护线程或用户线程,当正在运行的线程都是守护线程时,Java虚拟机退出。该方法必须在启动线程前调用。
join() 临时加入一个线程时可以调用join()方法
六、面试题
1、以下代码是否有错误?
1 class Test implements Runnable 2 { 3 public void run(Thread t) 4 {} 5 }
解析:第一行有错误,class应该被abstract修饰,public void run(Thread t)方法没有覆盖Runnable接口的run方法,是函数的重载,所以Test类中有抽象方法,是抽象类。
2、以下代码是否有错误,如果没有,运行结果是什么?
class ThreadTest { public static void main(String[] args) { new Thread(new Runnable() { public void run() { System.out.println("runnable run"); } }) { public void run() { System.out.println("subThread run"); } }.start(); } }
解析:没有错误,结果是subThread run,