Java 多线程编程

Java 多线程编程

目录

Java 多线程编程

一、进程与线程

进程

线程

简而言之:

二、多线程

(1)、什么是多线程

(2)、多线程好处

三、主线程

四、线程的创建和启动

(1)、继承Thread类创建线程

(2)、实现Runnable接口创建线程

(3)、直接调用run()和start()区别

(4)、比较两种创建线程的方式

五、线程的生命周期

六、线程调度

1、线程优先级

2、线程休眠

3、线程的强制运行

4、线程的礼让

操作练习

七、多线程共享数据引发的问题

(1)、同步方法

(2)、同步代码块

八、线程安全的类型

九、常见类型对比

十、线程的通信

sleep和wait的异同:

生产者和消费者


一、进程与线程

进程

应用程序的执行实例。

有独立的内存空间和系统资源

线程

CPU调度和分派的基本单位

进程中执行运算的最小单位,可完成一个独立的顺序控制流程

简而言之:

一个程序至少有一个进程,一个进程至少有一个线程。

线程的划分尺度小于进程,使得多进程程序的并发性高。

另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

 在Java中,每次程序运行至少启动2个线程:一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际上就是在操作系统中启动了一个进程。

二、多线程

(1)、什么是多线程

  • 如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为“多线程
  • 多个线程交替占用CPU资源,而非真正的并行执行

(2)、多线程好处

  • 充分利用CPU的资源
  • 简化编程模型
  • 带来良好的用户体验

三、主线程

Thread类

  • Java提供了java.lang.Thread类支持多线程编程

主线程

  • main()方法即为主线程入口
  • 产生其他子线程的线程
  • 必须最后完成执行,因为它执行各种关闭动作

运行结果:

四、线程的创建和启动

在Java中创建线程的两种方式:

继承java.lang.Thread类

实现java.lang.Runnable接口

使用线程的步骤:

(1)、继承Thread类创建线程

  • 创建一个新的类,继承 Thread 类
  • 必须重写 run() 方法,该方法是新线程的入口点,编写线程执行体
  • 创建线程对象,调用start()方法启动线程,而不是run()方法
package com.zb.one;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 1; i < 11; i++) {
            System.out.println(
                    Thread.currentThread().getName() + ":" + i);
        }
    }
}
package com.zb.one;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class Test {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); //启动线程
    }
}

运行结果:

多个线程交替执行,不是真正的“并行”

线程每次执行时长由分配的CPU时间片长度决定

package com.zb.one;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class Test {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();
        t2.start();

    }
}

运行结果:

(2)、实现Runnable接口创建线程

  • 定义MyRunnable类实现Runnable接口
  • 实现run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
package com.zb.runnable;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i < 11; i++) {
            System.out.println(
                    Thread.currentThread().getName() + ":" + i);
        }
    }
}
package com.zb.runnable;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class Test {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread myThread = new Thread(myRunnable, "张三");
        Thread myThread1 = new Thread(myRunnable, "李四");
        myThread.start();
        myThread1.start();
    }
}

运行结果:

(3)、直接调用run()和start()区别

(4)、比较两种创建线程的方式

继承Thread类

  • 编写简单,可直接操作线程
  • 适用于单继承

实现Runnable接口

  • 避免单继承局限性
  • 便于共享资源

五、线程的生命周期

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

六、线程调度

线程调度指按照特定机制为多个线程分配CPU的使用权

方       法

 说       明

setPriority(int  newPriority)

getPriority()

更改线程的优先级

获取线程的优先级

static void sleep(long millis)

在指定的毫秒数内让当前正在执行的线程休眠

void join()

等待该线程终止

static void yield()

暂停当前正在执行的线程对象,并执行其他线程

void interrupt()

中断线程

boolean isAlive()

测试线程是否处于活动状态

1、线程优先级

线程优先级由1 (Thread.MIN_PRIORITY )~ 10 (Thread.MAX_PRIORITY )表示,1最低,默认优先级为5

优先级高的线程获得CPU资源的概率较大

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

package com.zb.runnable;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class Test {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread myThread = new Thread(myRunnable, "张三");
        Thread myThread1 = new Thread(myRunnable, "李四");
        Thread myThread2 = new Thread(myRunnable, "王五");
        myThread.setPriority(Thread.MAX_PRIORITY);
        myThread1.setPriority(Thread.MIN_PRIORITY);
        myThread2.setPriority(3);
        myThread.start();
        myThread1.start();
        myThread2.start();
    }
}

2、线程休眠

让线程暂时睡眠指定时长,线程进入阻塞状态

睡眠时间过后线程会再进入可运行状态

public static void sleep(long millis)

millis为休眠时长,以毫秒为单位

调用sleep()方法需处理InterruptedException异常

package com.zb.thread;


import com.zb.one.MyThread;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class Test {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread1 t2 = new MyThread1();
        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread {

    @Override
    public void run() {
        for (int i = 1; i < 11; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3、线程的强制运行

使当前线程暂停执行,等待其他线程结束后再继续执行本线程

public final void join()
public final void join(long mills)
public final void join(long mills,int nanos)

millis:以毫秒为单位的等待时长

nanos:要等待的附加纳秒时长

需处理InterruptedException异常

package com.zb.thread;


import com.zb.one.MyThread;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class Test {
    public static void main(String[] args) {
        MyThread4 mt = new MyThread4();
        try {
            for (int i = 0; i < 10; i++) {
                if (i == 4) {
                    mt.start();
                    mt.join();
                }
                System.out.println("主线程开始执行--->" + (i + 1));
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


class MyThread4 extends Thread {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("强行插入的线程:" + (i + 1));
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

运行结果:

4、线程的礼让

暂停当前线程,允许其他具有相同优先级的线程获得运行机会

该线程处于就绪状态,不转为阻塞状态

public static void yield()

package com.zb.thread;


import com.zb.one.MyThread;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/16
 * @Version V1.0
 */
public class Test {
    public static void main(String[] args) {

        MyThread5 my5 = new MyThread5();
        Thread t1 = new Thread(my5, "线程A");
        Thread t2 = new Thread(my5, "线程B");
        t1.start();
        t2.start();


    }
}


class MyThread5 implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().
                    getName() + "正在运行:" + i);
            if (i == 3) {
                System.out.print("线程礼让:");
                Thread.yield();
            }
        }
    }
}

运行结果:

操作练习

使用多线程模拟多人徒步爬山

package com.zb.test;

import com.zb.demo.ClimbThread;

public class TestClimb {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ClimbThread yong=new ClimbThread(1, 20, "年轻人");
		ClimbThread old = new ClimbThread(3, 5, "老年人");
		yong.start();
		old.start();
	}

}

class ClimbThread extends Thread {

	private long time;
	private int num;

	public ClimbThread(long time, int num, String name) {
		// TODO Auto-generated constructor stub
		super.setName(name);
		this.time = time;
		this.num = num;
	}

	@Override
	public void run() {
		try {
			for (int i = 0; i < num; i++) {
				System.out.println(super.getName() + "爬了100米");
				Thread.sleep(time * 1000);
			}
		} catch (Exception e) {
			// TODO: handle exception
		}
		System.out.println(super.getName() + "爬完了" + num * 100 + "米");
	}

}

模拟叫号看病

package com.test;

public class Test01 {
    public static void main(String[] args) {

        MyThread01 myThread1 = new MyThread01();
        myThread1.setPriority(Thread.MAX_PRIORITY);
        myThread1.start();
        for (int i = 0; i < 50; i++) {
            System.out.println("普通号" + (i + 1) + "号病人在看病");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i == 9) {
                try {
                    myThread1.join();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}


class MyThread01 extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("特需号:" + (i + 1) + "号病人在看病");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

七、多线程共享数据引发的问题

(1)、同步方法

使用synchronized修饰的方法控制对类成员变量的访问

访问修饰符 synchronized 返回类型 方法名(参数列表){……}
        或者
synchronized 访问修饰符 返回类型 方法名(参数列表){……}

synchronized就是为当前的线程声明一个锁

package com.zb.two;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/17
 * @Version V1.0
 */


public class Test {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        DataSource dataSource = new DataSource();
        Thread zhangsan = new Thread(dataSource, "张三");
        Thread huangniu = new Thread(dataSource, "黄牛");
        zhangsan.start();
        huangniu.start();
    }

}


class DataSource implements Runnable {

    int num = 0;
    int count = 10;
    boolean val = true;

    @Override
    public void run() {
        while (val) {
            buy();
        }
    }

    public synchronized void buy() {
        if (count <= 0) {
            val = false;
            return;
        }
        num++;
        count--;
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "张票,还剩" + count + "张");
    }

}

运行结果:

(2)、同步代码块

使用synchronized关键字修饰的代码块

synchronized(syncObject){
    //需要同步的代码
}

syncObject为需同步的对象,通常为this

效果与同步方法相同

public void run() {
    while (true) {
		synchronized (this) {   //同步代码块
		// 省略修改数据的代码......
		// 省略显示信息的代码......
        }
    }
}

多个并发线程访问同一资源的同步代码块时

  • 同一时刻只能有一个线程进入synchronized(this)同步代码块
  • 当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定
  • 当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码
package com.zb.test;

import com.zb.demo.DataSource;

public class TestDataSource {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		DataSource dataSource = new DataSource();
		Thread zhangsan = new Thread(dataSource, "张三");
		Thread huangniu = new Thread(dataSource, "黄牛");
		zhangsan.start();
		huangniu.start();
	}

}

class DataSource implements Runnable {

	int num = 0;
	int count = 10;
	boolean val = true;

	@Override
	public void run() {
		System.out.println("代码块");
		while (val) {
			synchronized (this) {
				if (count <= 0) {
					break;
				}
				num++;
				count--;
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "张票,还剩" + count + "张");
			}
		}
	}

}

八、线程安全的类型

ensureCapacityInternal(),它的作用就是判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容。

这样也就出现了第一个导致线程不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:

  1. 列表大小为9,即size=9
  2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
  3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
  4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
  5. 线程B也发现需求大小为10,也可以容纳,返回。
  6. 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
  7. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:

elementData[size] = e;
size = size + 1;


在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:

  1. 列表大小为0,即size=0
  2. 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
  3. 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
  4. 线程A开始将size的值增加为1
  5. 线程B开始将size的值增加为2
  6. 这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。

九、常见类型对比

十、线程的通信

通信常用方法:

通信方法描述
wait()一旦执行此方法,当前线程就进入阻塞状态,并释放锁
notify一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程
notifyAll一旦执行此方法,就会唤醒所有被wait()的线程

使用前提:这三个方法均只能使用在同步代码块或者同步方法中。

sleep和wait的异同:

相同点:一旦执行方法以后,都会使得当前的进程进入阻塞状态
不同点:
1.两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
2.调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
3.关于是否释放锁,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放

生产者和消费者

什么是等待/通知机制?

使用wait/notify方法实现线程间通信,要注意以下两点:
(1) wait和notify必须配合synchronized关键字使用
(2)wait方法释放锁,notify方法不释放锁

  • 方法wait()的作用就是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断终止。在调用wait()之前,必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。在执行wait()方法后,当前线程释放锁。在wait返回前,线程与其他线程竞争重新获得锁。如果调用wait()时没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch语句进行捕捉异常。
  • 方法notify()也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。需要说明的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才能获取该对象锁。当第一个获得了该对象锁的wait线程执行运行完毕以后,他会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。
     
package com.zb.three;

/**
 * @author XiaChuanKe
 * @Description TODO
 * @Date 2020/3/17
 * @Version V1.0
 */

public class ConsumerAndProducerTest {
    public static void main(String[] args) throws InterruptedException {
        SyncStack lanzi = new SyncStack();
        Producer producer = new Producer(lanzi);
        Comsuer comsuer = new Comsuer(lanzi);
        producer.start();
        Thread.sleep(8000);
        comsuer.start();
    }
}

class WoTo {
    int id;

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "第" + id + "个窝头";
    }
}
/**
 * 容器类型
 * @author Administrator
 *
 */
class SyncStack {
    //容器的大小
    WoTo[] woTos = new WoTo[6];
    //当前存储的标记
    int index = 0;
    /*生产者调用的存储方法*/
    public synchronized void push(WoTo woto) throws Exception {
        //当前篮子为满的状态
        if (index == woTos.length) {
            System.out.println("厨师休息会,没有人吃!");
            wait();//当前线程等待
        }
        //将做好的窝头存储到容器总
        woTos[index] = woto;
        //标记递增
        index++;
        //通知正在等待的消费者开始工作
        notify();
    }
    /*消费者方法*/
    public synchronized WoTo pop() throws Exception {
        //当前容器为空的时候
        if (index == 0) {
            System.out.println("消费者休息会,没有做出窝头!");
            wait();//当前线程停止工作,设置成等待状态
        }
        index--;//标记递减
        //获取容器中最顶层的产品
        WoTo woTo = woTos[index];
        //容器中有空的位置, 通知生产者继续工作
        notify();
        return woTo;
    }
}
/*生产者*/
class Producer extends Thread {
    //需要的容器对象
    private SyncStack stack;

    public Producer(SyncStack stack) {
        this.stack = stack;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 20; i++) {
                //创建产品
                WoTo woTo = new WoTo();
                woTo.id = i + 1;
                //将产品存储到容器中
                stack.push(woTo);
                System.out.println("厨师做好了" + woTo);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
/*消费者对象*/
class Comsuer extends Thread {
    //容器对象
    private SyncStack stack;

    public Comsuer(SyncStack stack) {
        this.stack = stack;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 20; i++) {
                //返回产品信息
                WoTo woto = stack.pop();
                System.out.println("消费者吃掉" + woto);
                Thread.sleep(3000);
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

运行结果:

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值