Java多线程学习总结(全面的万字长篇)

Java多线程学习(全面万字长篇)

  • 前言
  • 线程的创建和使用
    • 线程的创建和启动
    • 创建线程的两种方式
    • Thread类相关方法
    • JDK5.0新增创建线程方式
    • 线程的优先级
  • 线程的生命周期
  • 线程的同步(解决共享资源竞争)
    • 方式一:同步代码块
    • 方式二:同步方法
    • 方式三:使用显示的Lock对象
    • synchronized 与 Lock 的对比
  • 线程的通信
  • 生产者和消费者问题

前言

本篇文章只是对多线程做一个简单较全面了解,并不深入探讨
什么时候需要使用多线程?
1、当程序需要同时执行两个或多个任务时。
2、程序需要实现一些需要等待的任务时,如用户输入、文件读写
操作、网络操作、搜索等。
3、需要一些后台运行的程序时。

线程的创建和使用

线程的创建和启动

  1. Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
    类来体现。
  2. Thread类的特性
  • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常
    把run()方法的主体称为线程体
  • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

    创建线程的两种方式

方式一:继承Thread类

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。
class Even extends Thread{
    @Override
    public void run(){
        for(int i=0; i<10;i++){
            if(i%2 != 0)
                System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

方式二:实现Runnable接口

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
class Odd implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<10;i++){
            if(i%2==0)
                System.out.println(Thread.currentThread().getName()+":"+i);
            Thread.yield();   //让步
            /*try {
                Thread.sleep(100);   //休眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        }
    }
}
public class ThreadDome01 {
    public static void main(String[] args) {
        Even even = new Even();
        even.start();
        Odd odd = new Odd();
        Thread thread = new Thread(odd);
        thread.start();
    }
}
/*	[补充]:1.在run()执行完之后,线程就死亡了
*		2.even.start()已经创建了一个线程,不能再写even.start(),如果要再创建
*		线程,就必须再new一个对象
*/

输出结果:
Thread-1:0
Thread-0:1
Thread-0:3
Thread-1:2
Thread-0:5
Thread-1:4
Thread-0:7
Thread-1:6
Thread-0:9
Thread-1:8

推荐使用实现Runnable的方式

  1. 避免了单继承的局限性
  2. 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线
    程来处理同一份资源。

Thread类相关方法

  1. void start() : 启动线程,并执行对象的run()方法
  2. run(): 线程在被调度时执行的操作
  3. String getName(): 返回线程的名称
  4. void setName(String name):设置该线程名称
  5. static Thread currentThread(): 返回当前线程。在Thread子类中就
    是this,通常用于主线程和Runnable实现
  6. static void yield():线程让步
    - 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    - 若队列中没有同优先级的线程,忽略此方法
  7. join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将
    被阻塞,直到 join() 方法加入的 join 线程执行完为止
    - 低优先级的线程也可以获得执行
  8. static void sleep(long millis):(指定时间:毫秒)
    令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后
    重排队。

JDK5.0新增创建线程方式

新增方式一:使用线程池
线程池相关API:

  1. JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable

  • < T> Future< T > submit(Callable< T> task):执行任务,有返回值,一般又来执行Callable

  • void shutdown() :关闭连接池

  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

  • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池

  • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池

  • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

      java SE5的Java.util.concurrent包中的执行器可以为我们管理Thread对象,从而简化并发编程.Executor无须显示地管理线程的生命周期。
    
class Ex3RunnerA implements Runnable {
    public Ex3RunnerA() {
        System.out.println("Constructing Ex3RunnerA");
    }
    public void run() {
        for(int i = 0; i < 3; i++) {
            System.out.println("Hi from Ex3RunnerA");
            Thread.yield();
        }
        System.out.println("Ex3RunnerA task complete.");
        // return;
    }
}

class Ex3RunnerB implements Runnable {
    public Ex3RunnerB() {
        System.out.println("Constructing Ex3RunnerB");
    }
    public void run() {
        for(int i = 0; i < 3; i++) {
            System.out.println("Hi from Ex3RunnerB");
            Thread.yield();
        }
        System.out.println("Ex3RunnerB task complete.");
        //return;
    }
}

class Ex3RunnerC implements Runnable {
    public Ex3RunnerC() {
        System.out.println("Constructing Ex3RunnerC");
    }
    public void run() {
        for(int i = 0; i < 3; i++) {
            System.out.println("Hi from Ex3RunnerC");
            Thread.yield();
        }
        System.out.println("Ex3RunnerC task complete.");
        //return;
    }
}
public class Ex3 {
    public static void main(String[] args) {
    	// ExecutorService对象是通过Executors的静态方法创造的
        ExecutorService exec1 = Executors.newCachedThreadPool();
        exec1.execute(new Ex3RunnerA());
        exec1.execute(new Ex3RunnerB());
        exec1.execute(new Ex3RunnerC());
        exec1.shutdown();
        ExecutorService exec2 = Executors.newFixedThreadPool(3);
        exec2.execute(new Ex3RunnerA());
        exec2.execute(new Ex3RunnerB());
        exec2.execute(new Ex3RunnerC());
        exec2.shutdown();
        ExecutorService exec3 = Executors.newSingleThreadExecutor();//就像是线程数为1的FixedThreadPool
        exec3.execute(new Ex3RunnerA());
        exec3.execute(new Ex3RunnerB());
        exec3.execute(new Ex3RunnerC());
        exec3.shutdown();
    }
}

新增方式二:实现Callable接口
与使用Runnable相比, Callable功能更强大些

  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结
class TaskWithResult implements Callable<String> {
    private int id;
    public TaskWithResult(int id){
        this.id = id;
    }
    @Override
    public String call(){
        return "result of TaskWithResult "+id;
    }
}
public class Ex5 {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        ArrayList<Future<String>> results= new ArrayList<>();
        for(int i=0; i<10;i++){
            results.add(exec.submit(new TaskWithResult(i)));//submit()方法会产生Future对象
        }
        for(Future<String> fs : results){
            try {
                System.out.println(fs.get());//返回的结果
            }catch (InterruptedException e){
                System.out.println(e);
                return;
            }catch (ExecutionException e){
                System.out.println(e);
                return;
            }finally {
                exec.shutdown();
            }
        }
    }
}

submit()方法会产生Future对象,它用Callable返回的结果的特定类型进行了参数化,可以用isDone()方法来查询Future是否已经完成。当完成任务时,它具有一个结果,可以调用get()来获取该结果。

输出结果:
result of TaskWithResult 0
result of TaskWithResult 1
result of TaskWithResult 2
result of TaskWithResult 3
result of TaskWithResult 4
result of TaskWithResult 5
result of TaskWithResult 6
result of TaskWithResult 7
result of TaskWithResult 8
result of TaskWithResult 9

线程的优先级

  1. 线程的优先级等级
    - MAX_PRIORITY:10
    - MIN _PRIORITY:1
    - NORM_PRIORITY:5
  2. 涉及的方法
  • getPriority() :返回线程优先值
  • setPriority(int newPriority) :改变线程的优先级
  1. 说明
  • 线程创建时继承父线程的优先级
  • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

线程的生命周期

Java多线程学习总结(全面的万字长篇)_第1张图片

线程的同步(解决共享资源竞争)

首先举个例子——模拟火车站售票程序,开启三个窗口售票。
class Ticket implements Runnable {
    private int tick = 100;
    public void run() {
        while (true) {
            if (tick > 0) {
                try {
                    Thread.sleep(100);  //将线程安全问题展现更清楚
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread
                        ().getName() + "售出车票,tick号为:" + tick--);
            } else
                break;
        }
    }
}
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);
        t1.setName("t1窗口");
        t2.setName("t2窗口");
        t3.setName("t3窗口");
        t1.start();
        t2.start();
        t3.start();
    }
}
部分输出结果:
t2窗口售出车票,tick号为:4
t1窗口售出车票,tick号为:4
t3窗口售出车票,tick号为:3
t1窗口售出车票,tick号为:3
t2窗口售出车票,tick号为:1
t3窗口售出车票,tick号为:2

问题:多个窗口售票出了相同的票号。
问题原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

防止线程冲突的方法就是当资源被一个任务使用时,在其上加上锁。都一般采取序列化访问共享资源 的方案。

方式一:同步代码块

方式一:同步代码块
*   synchronized(同步监视器){
*   //需要同步的代码
*   }
*   说明:1、需要同步的代码即使操作共享数据的代码
*        2、共享数据:多个线程共同操作的变量,在本例子中tick
*        3、同步监视器,俗称:锁。任何一个类的对象,都可以充当索。
*       要求:多个线程必须要共用同一把锁。

使用同步代码块的方式来改改上面的火车卖票的程序。
下面是实现Runnable接口的方式

class Ticket implements Runnable {
    private int tick = 100;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
           // synchronized (this){  //对于实现同步监视器是当前对象t。一般都写this
            synchronized (obj) {    //同步监视器是obj,任何对象都可以当作索。但必须多个线程共用一把锁
                if (tick > 0) {
                    try {
                        Thread.sleep(100);  
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread
                            ().getName() + "售出车票,tick号为:" + tick--);
                } else
                    break;
            }
      //   }
        }
    }
}

需要注意的是同步监视器(也就是锁),同步监视器可以是任何对象,但必须是多个线程共用一把锁对于实现Runnable接口的方式,上面两个都可以,一般用synchronized (this){},
下面是对于继承的方式
继承的方式和实现接口的方式是有所不同的
由于继承方式需要创建3个Ticcket1对象,所以不能用synchronized (this){},
可以将Ticket1类作为同步监视器,即 synchronized (Ticket1.class){ },也可以用其它对象,但是需要声明为静态static的。

class Ticket1 extends Thread {
    private static int tick = 100;
   // private static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
             synchronized (Ticket1.class){  //和下面的都可以
          //  synchronized (obj) {    //同步监视器是obj,
                if (tick > 0) {
                    try {
                        Thread.sleep(100);  //将线程安全问题展现更清楚
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread
                            ().getName() + "售出车票,tick号为:" + tick--);
                } else
                    break;
            }
            //   }
        }
    }
}

class TicketDemo02 {
    public static void main(String[] args) {
        Ticket1 t1 = new Ticket1();
        Ticket1 t2 = new Ticket1();
        Ticket1 t3 = new Ticket1();
        t1.setName("t1窗口");
        t2.setName("t2窗口");
        t3.setName("t3窗口");
        t1.start();
        t2.start();
        t3.start();
    }
}

总的来说就是同步监视器需要是多个线程共用一个

方式二:同步方法

同步方法就是如果操作共享资源的刚好是一个方法,即可以在方法上加上synchronized 关键字就行。
比如对于实现的方式

public synchronized void method(){}

对于继承的方式 需要加上static。

public static synchronized void show(){}

对于同步方法的总结:

  • 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
  • 非静态的同步方法,同步监视器是:this
  • 静态的同步方法,同步监视器是:当前类本身。

方式三:使用显示的Lock对象

javaSE5的Java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显示的互斥机制。Lock对象必须显示地创建、锁定和释放。因此,它与内建的锁形式相比。代码缺少点优雅,但是对于某些问题更有优势。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的
工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象
加锁,线程开始访问共享资源之前应先获得Lock对象。

class A{
	private ReentrantLock lock = new ReenTrantLock();
	//private Lock lock = new ReenTrantLock();//也可以
	public void m(){
		lock.lock();
		try{
		//保证线程安全的代码;
		}
		finally{
			lock.unlock(); 
		}
	}
}
//注意:如果同步代码有异常,要将unlock()写入finally语句块

注意 如果方法有返回值,return必须在try子句中出现,以确保unlock()不会过早发生。

synchronized 与 Lock 的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是
    隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
    更好的扩展性(提供更多的子类)
    优先使用顺序
    Lock ——> 同步代码块(已经进入了方法体,分配了相应资源) ——> 同步方法
    (在方法体之外)

线程的通信

还是以一个例子来讲解:使用两个线程打印 1-100。线程1, 线程2 交替打印
class Communication implements Runnable {
    int i = 1;
    public void run() {
        while (true) {
            synchronized (this) {
                notify();//因为省略了this,相当于this.notify(),必须是同步监视器调用。
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + i++);
                } else
                    break;
                try {
                    wait();//省略了this,相当于this.wait(),必须是同步监视器调用。
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Communication c = new Communication();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

涉及到的3个方法:

  • wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器。
  • notify(): 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait(),就唤醒优先级较高的。
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

说明
这三个方法必须使用在同步代码块或同步方法中。
这三个方法的调用者必须是同步代码块或同步方法中的同步监视器
否则都会报java.lang.IllegalMonitorStateException异常。

如果同步监视器是其它对象:


private Object obj = new Object();
//省略代码...
 synchronized (obj) {//obj作为同步监视器
	obj.notify();
	//省略代码...
	obj.wait();
	//....
}

正因为如此,这三个方法声明在Object类中。
再来说一说wait()和sleep()的区别:

  1. 两个方法声明位置不同。Thread类中声明sleep(),Object类中声明wait();
  2. sleep()可以在任何需要的场景使用,而wait()必须使用在同步代码块或同步方法中
  3. 如果两个方法都是用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放同步监视器。

生产者和消费者问题

通过上面的学习,可以敲一下多线程的经典问题来学以致用。

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,
店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;
如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

代码:

class Clerk { // 售货员
    private int product = 0;
    public synchronized void addProduct() {
        if (product >= 20) {
            try {
                wait();
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        } else {
            product++;
            System.out.println("生产者生产了 第" + product + "个产品");
                    notifyAll();
        }
    }
    public synchronized void getProduct() {
        if (this.product <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("消费者取走了第" +
                    product + "个产品");
            product--;
            notifyAll();
        }
    }
}
class Productor implements Runnable { // 生产者
    Clerk clerk;
    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }
    public void run() {
        System.out.println("生产者开始生产产品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.addProduct();
        }
    }
}
class Consumer implements Runnable { // 消费者
    Clerk clerk;
    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    public void run() {
        System.out.println("消费者开始取走产品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.getProduct();
        }
    }
}

public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Thread productorThread = new Thread(new Productor(clerk));
        Thread consumerThread = new Thread(new Consumer(clerk));
        productorThread.start();
        consumerThread.start();
    }

}

资料:宋红康java多线程讲解

如果读者大大们感觉写的可以,希望给个小小的点赞,你们的鼓励就是我前进的动力。

你可能感兴趣的:(Java,java,多线程)