JAVA多线程新手指引
一、简述进程与线程的关系
说到多线程就不得不说进程,那么什么是进程:简单的来说就是正在进行中的程序。
在windows中,我们可以直观的看到的正在运行中的程序,即进程,如图:
那么什么又是线程呢?
线程其实就是进程的最小工作单位。是进程中一个负责程序执行的控制单元(执行路径), 一个进程中可以多执行路径,称之为多线程。
一个进程成至少要有一个线程。
在JAVA中开启多线程的目的是什么?
就是为了同时运行多部分代码,提高工作效率。
而每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
其实应用程序的执行都是cpu在做着快速的切换完成的,这个切换是随机的,即所有应用程序在运行的过程中都在争夺cpu的执行权,谁拿到这个执行权那么谁就执行相应的代码,执行自己要执行的任务。
在JVM启动时,就启动了多个线程,即便是最简单的代码,我们也能够直观的分析出两个线程
1,执行main函数的线程。
该线程的任务代码都定义在main函数中
2,负责垃圾回收的线程。
二、创建线程
创建线程方式一:继承Thread类
步骤:
1,定义一个类继承Thread类
2,覆盖Thread类中的run方法。
创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行。
而运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中。
而自定义的线程它的任务哪里呢?
Thread类用于描述线程,线程是需要任务的,所以Thread类也有对任务的描述。
这个任务就通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程任务的函数。
run方法中定义的就是线程要运行的任务代码。
开启线程是为了制定代码,所以只有继承Thread类,并复写run方法。
将运行的代码定义在run方法中即可。
简单代码示例:
public class ExtendsThreadDemo { public static void main(String[] args) { Student s1 = new Student(); Student s2 = new Student(); s1.start(); s2.start(); } } class Student extends Thread{ public void run(){ while(true){ System.out.println(Thread.currentThread().getName()+".....student study!!!"); } } }
3,直接创建Thread类的子类对象创建线程。
4,调用对象的start方法开启线程并调用线程的任务run方法执行。
补充:可以通过Thread的getName获取线程的名称 : Thread-编号(从0开始)主线程的名字就是main
调用run和调用start有什么区别?
调用run与调用一个方法是相同的,而调用start是调用一个线程来执行run方法。
创建线程的第二种方式:实现Runnable接口
1,定义类实现Runnable接口。
2,覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3,通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
原因是线程的任务都封装在Runnable接口子类对象的run方法中。所以要再线程对象创建时就必须明确要运行的任务。
4,调用线程对象的start方法开启线程。
简单代码示例:
public class ImplementsRunnableDemo { public static void main(String[] args) { Student s1 = new Student(); Thread t1 = new Thread(s1); Thread t2 = new Thread(s1); t1.start(); t2.start(); } } class Student implements Runnable{ public void run(){ while(true){ System.out.println(Thread.currentThread().getName()+".....student study!!!"); } } }
实现Runnable接口的好处:
1,将线程的任务从线程的子类中分离出来,进行了单独的封装。
按照面向对象的思想将任务封装成对象。
2,避免了java单继承的局限性。
所以,创建线程的第二种方式较为常用。
线程安全问题产生的原因:
1,多个线程在操作共享的数据。
2,操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
解决思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程是不可以参与运算的。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
在java中,用同步代码块就可以解决这个问题。
同步代码块的格式:
synchronized(对象){
需要被同步的代码;
}
同步的好处:
解决了线程的安全问题。
同步的弊端:
相对降低了效率,因为同步外的线程都会判断同步锁。判断后还不能执行,相当于进行的是无效判断。
同步的前提:
同步中必须有多个线程并使用同一个锁。
同步函数
格式: public synchronized void 函数名( ){ }
注:同步函数的锁是this
同步函数和同步代码块的区别:
同步函数的锁是固定的this
同步代码块的锁是任意的对象
在开发中,建议使用同步代码快,同步函数可以作为同步代码快的简写形式。
静态的同步函数
格式: public static synchronized void 函数名( ){ }
静态的同步函数使用的锁是该函数所属的字节码文件对象,可以用getClass方法获取,也可以用 当前类名.class 形式表示
三、多线程下的单例设计模式
饿汉式
饿汉式的写法比较安全和高效,也是我们在开发过程中常用的写法。
代码示例:
wait和sleep的区别:
class Single { private static final Single s = new Single();//类加载的时候就创建了s,不会有争执和分歧 private Single(){} public static Single getInstance() { return s; } }
懒汉式
原写法:
class Single { private static Single s = null; private Single(){} public static Single getInstance() { if (s == null) s = new Single(); return s; } }
多线程写法:
class Single { private static Single s = null; private Single(){} public static Single getInstance() { if(s == null){ //解决效率问题 syncchronized(Single.class) { //解决安全问题 if (s == null) s = new Single(); return s; } } } }
四、死锁
常见情景之一就是同步之间的嵌套。
示例代码如下:
class DeadLock { public static void main(String[] args) { Demo d1 = new Demo(true); Demo d2 = new Demo(false); Thread t1 = new Thread(d1); Thread t2 = new Thread(d2); t1.start(); t2.start(); System.out.println(Thread.currentThread().getName()+"........"+"over"); } } class Demo implements Runnable { private boolean flag; Demo(boolean flag){ this.flag = flag; } public void run() { if(flag) { while(true) { synchronized(MyLock.lock2){ System.out.println("aaaaaa111111锁"+Thread.currentThread().getName()); synchronized(MyLock.lock1){ System.out.println("aaaaaa222222锁"+Thread.currentThread().getName()); } } } } else{ while(true) { synchronized(MyLock.lock1){ System.out.println("bbbbbbb111111锁"+Thread.currentThread().getName()); synchronized(MyLock.lock2){ System.out.println("bbbbbbbb222222锁"+Thread.currentThread().getName()); } } } } } } class MyLock { public static final Object lock1 = new Object(); public static final Object lock2 = new Object(); }
五、线程间的通讯:
个人理解:多个线程在处理同一资源,但是任务却不同。
等待和唤醒机制
涉及的方法:
1,wait();
:让线程处于冻结状态,线程会释放cpu的执行权和执行资格,被wait的线程会被存储到线程池中。
2.notify();
:用于唤醒线程池中的一个线程(任意的,无顺序)。
3,notifyAll();
:用于唤醒线程池中的所有线程。让线程具备了执行资格,处于运行状态或线程阻塞状态。
这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法。必须要明确到底操作的是哪个锁上的线程。可以理解为以锁来标记线程池。
为什么操作线程的方法wait () notiry() notifyAll()定义在了Object类中,而没有定义在Thread类中?
因为这些方法是监视器的方法,监视器其实就是锁,锁可以是任意的对象,任意的对象调用的方法一定定义在Object类中。
等待唤醒机制的经典案例:生产者与消费者
单生产者与单消费者代码示例:
public class ProducerAndConsumer { public static void main(String[] args) { Goods g = new Goods(); Producer p = new Producer(g); Consumer c = new Consumer(g); Thread t1 = new Thread(p); Thread t2 = new Thread(c); t1.start(); t2.start(); } } class Goods{ private String name; private int count = 1; private boolean flag = false; public synchronized void set(String name){ if(flag){ try { this.wait(); } catch (InterruptedException e) {} } this.name = name + count; count++; System.out.println(Thread.currentThread().getName()+"--生产--"+this.name); flag = true; notify(); } public synchronized void out(){ if(!flag){ try { this.wait(); } catch (InterruptedException e) {} } System.out.println(Thread.currentThread().getName()+"--消费--------"+this.name); flag = false; notify(); } } class Producer implements Runnable{ private Goods g; Producer(Goods g) { this.g = g; } public void run(){ while (true) { g.set("馒头"); } } } class Consumer implements Runnable{ private Goods g; Consumer(Goods g) { this.g = g; } public void run(){ while (true) { g.out(); } } }
多生产者与多消费者
if判断标记,只有一次,会导致不该运行的线程运行了,出现了数据错误的情况。while判断标记,解决了线程获取执行权后,是否要运行!
notify:只能唤醒一个线程,如果本方唤醒了本方,没有意义,而且while判断标记+notify会导致死锁
notifyAll解决了本方线程一定会唤醒对方线程的问题。
代码示例一:
public class ProducerAndConsumer { public static void main(String[] args) { Goods g = new Goods(); Producer p = new Producer(g); Consumer c = new Consumer(g); Thread t0 = new Thread(p); Thread t1 = new Thread(c); Thread t2 = new Thread(p); Thread t3 = new Thread(c); t0.start(); t1.start(); t2.start(); t3.start(); } } class Goods{ private String name; private int count = 1; private boolean flag = false; public synchronized void set(String name){ while(flag){ try { this.wait(); } catch (InterruptedException e) {} } this.name = name + count; count++; System.out.println(Thread.currentThread().getName()+"--生产--"+this.name); flag = true; notifyAll(); } public synchronized void out(){ while(!flag){ try { this.wait(); } catch (InterruptedException e) {} } System.out.println(Thread.currentThread().getName()+"--消费--------"+this.name); flag = false; notifyAll(); } } class Producer implements Runnable{ private Goods g; Producer(Goods g) { this.g = g; } public void run(){ while (true) { g.set("馒头"); } } } class Consumer implements Runnable{ private Goods g; Consumer(Goods g) { this.g = g; } public void run(){ while (true) { g.out(); } } }
jdk1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显式动作
Lock接口:出现替代了同步代码块或者同步函数,将同步的隐式锁操作编程了显式锁操作,同时更为灵活,可以一个锁上加上多组监视器
lock():获取锁
unlock():释放锁,通常需要定义finally代码块中。
Condition接口:出现替代了Object中的wait / notify / notifyAll 方法
将这些监视器方法单独进行了封装,变成了Condition监视器对象。
可以和任意锁进行组合。
await();
signal();
signalAll();
代码示例二:
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ProducerAndConsumer { public static void main(String[] args) { Goods g = new Goods(); Producer p = new Producer(g); Consumer c = new Consumer(g); Thread t0 = new Thread(p); Thread t1 = new Thread(c); Thread t2 = new Thread(p); Thread t3 = new Thread(c); t0.start(); t1.start(); t2.start(); t3.start(); } } class Goods{ private String name; private int count = 1; private boolean flag = false; Lock lock = new ReentrantLock(); Condition c1 = lock.newCondition(); Condition c2 = lock.newCondition(); public void set(String name){ lock.lock(); try{ while(flag){ try{c1.await(); } catch (InterruptedException e) {} } this.name = name + count; count++; System.out.println(Thread.currentThread().getName()+"--生产--"+this.name); flag = true; c2.signal(); }finally{ lock.unlock(); } } public void out(){ lock.lock(); try{ while(!flag){ try {c2.await(); } catch (InterruptedException e) {} } System.out.println(Thread.currentThread().getName()+"--消费--------"+this.name); flag = false; c1.signal(); } finally{ lock.unlock(); } } } class Producer implements Runnable{ private Goods g; Producer(Goods g) { this.g = g; } public void run(){ while (true) { g.set("馒头"); } } } class Consumer implements Runnable{ private Goods g; Consumer(Goods g) { this.g = g; } public void run(){ while (true) { g.out(); } } }
补充:
wait和sleep的区别:
1,wait可以指定时间也可以不指定。
sleep必须指定时间。
2,在同步中时,对cpu的执行权和锁的处理不同。
wait:释放执行权,释放锁。
sleep:释放执行权,不释放锁。
停止线程方法:
run方法结束。(stop等方法已过时,且不安全)
那么如何控制线程的任务结束?
任务中都会有循环结构,只要控制住循环就可以结束任务。
控制循环通常就用定义标记来完成,但是如果线程处于了冻结状态,无法读取标记,那么就无法结束任务。此时可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备cpu的执行资格,但是强制动作会发生InterruptException,要进行处理。