多线程是java学习中重要的一部分,我们会通过多线程实现同时操作同一资源的程序
在了解多线程之前我们先学习一些基本知识:
进程:是正在运行的程序
线程:是进程中的单个顺序控制流,是一条执行路径
方法1:继承Thread类
两个小问题:
下面给出示例代码:
public class Demo1 {
public static void main(String[] args) {
//首先创建对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
my2.start();
}
}
public class MyThread extends Thread {
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<500;i++){
System.out.println(i);
}
}
}
方法2:实现Runnable接口
采用接口的好处:
下面给出示例代码:
public class Demo1 {
public static void main(String[] args) {
//首先创造MyRunnable类
MyRunnable my = new MyRunnable();
//然后创建Thread对象
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
//可以采用带线程名的方法创造对象
Thread t3 = new Thread(my,"吕小布");
//下面正常运行即可
t1.start();
t2.start();
t3.start();
}
}
public class MyRunnable implements Runnable{
//重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//这里没有继承Thread,所以不能直接使用getName
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
Thread类中设置和获得多线程名称的方法:
如何获得main()方法的线程名称?
下面给出示例代码:(setName和getName)
public class Demo1 {
public static void main(String[] args) {
//首先创建对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//我们可以通过setName()方法改变线程名
my1.setName("吕小布");
my2.setName("吕大布");
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
my2.start();
}
}
public class MyThread extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<500;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
下面给出示例代码:(通过构造方法重写线程名以及获得main的线程名)
public class Demo1 {
public static void main(String[] args) {
//我们可以直接采用带参构造创造有线程名的线程
//但注意:需要在MyThread里重写构造方法并super父类name
MyThread my1 = new MyThread("吕小布");
MyThread my2 = new MyThread("吕大布");
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
my2.start();
//这里再多讲一个Thread的Static方法
//currentThread()是static方法,可以获得其当前运行的线程
System.out.println(Thread.currentThread().getName());
}
}
public class MyThread extends Thread{
//重写构造方法
public MyThread(){
}
public MyThread(String name){
super(name);
}
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<10;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
线程有两种调度模型:
Java采用的是抢占式调度模型
假如计算机只有一个CPU,那么CPU在某一时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性的。
Thread类中设置和获取优先级的方法:
线程默认优先级是5,线程优先级范围是1~10;线程优先级高仅仅代表线程获得时间片的几率高。
下面给出示例代码:
public class Demo1 {
public static void main(String[] args) {
//首先创建对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
MyThread my3 = new MyThread();
//我们可以通过setName()方法改变线程名
my1.setName("刘备");
my2.setName("关羽");
my3.setName("张飞");
//我们先来查看未设置时优先级度数以及最高最低优先级(最低1,最高10,默认5)
System.out.println(my1.getPriority());
System.out.println(Thread.NORM_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);
System.out.println(Thread.MAX_PRIORITY);
//然后我们通过修改优先级控制线程顺序
my1.setPriority(10);
my2.setPriority(5);
my3.setPriority(1);
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
my2.start();
my3.start();
}
}
public class MyThread extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<10;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
下面给出线程控制相关方法:
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将该线程标记为守护线程,当运行的线程都是守护线程时,程序终止 |
下面给出示例代码:(sleep方法)
public class Demo1 {
public static void main(String[] args) {
//首先创建对象
Thread1 my1 = new Thread1();
Thread1 my2 = new Thread1();
Thread1 my3 = new Thread1();
//我们可以通过setName()方法改变线程名
my1.setName("刘备");
my2.setName("孙权");
my3.setName("曹操");
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
//这里数据出来就会每隔1s出来一次,且每次不是同个进程连续执行
my1.start();
my2.start();
my3.start();
}
}
//这里我们讲解sleep
public class Thread1 extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<500;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
//我们在输出后,让他们等到1s(注意这里单位是ms)
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
下面给出示例代码:(join方法)
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
//首先创建对象
Thread2 my1 = new Thread2();
Thread2 my2 = new Thread2();
Thread2 my3 = new Thread2();
//我们可以通过setName()方法改变线程名
my1.setName("刘备");
my2.setName("孙权");
my3.setName("曹操");
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my1.start();
//这里使用join()方法,我们使用join之后,只有这个进程结束之后,才继续运行其他部分
my1.join();
my2.start();
my3.start();
}
}
public class Thread2 extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<10;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
下面给出示例代码:(setDaemon方法)
public class Demo3 {
public static void main(String[] args) {
//首先创建对象
Thread3 my2 = new Thread3();
Thread3 my3 = new Thread3();
//我们可以通过setName()方法改变线程名
my2.setName("关羽");
my3.setName("张飞");
//我们设置主线程main改名为刘备
Thread.currentThread().setName("刘备");
//我们把关羽和张飞设置为守护线程
my2.setDaemon(true);
my3.setDaemon(true);
//调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
my2.start();
my3.start();
//我们给主线程设置一些动作
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//然后当我们运行后会发现,主线程完全运行,然后守护线程逐渐死亡不运行完毕
}
}
public class Thread3 extends Thread{
//重写run方法,实现线程化
@Override
public void run() {
for (int i=0;i<100;i++){
//我们可以采用getName()方法获得线程名
System.out.println(getName() + ":" + i);
}
}
}
这里我们给出示例图(来自b站黑马程序员)
需求:某电影院目前正在上映国产大片,共100张票,而它有三个窗口卖票,请设计一个程序模拟
下面给出示例代码:
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
SellTicket st = new SellTicket();
//创建售卖机
Thread sell1 = new Thread(st,"售卖机1号");
Thread sell2 = new Thread(st,"售卖机2号");
Thread sell3 = new Thread(st,"售卖机3号");
//开始运行即可
sell1.start();
sell2.start();
sell3.start();
}
}
public class SellTicket implements Runnable{
//这里设置共有ticket
public static int ticket = 100;
//重构方法
@Override
public void run() {
//因为程序一直运行,需要在死循环里进行
while (true){
//当票还有时,售卖,ticket减一
if (ticket>0){
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
}
}
这里我们稍微给出编译结果:
售卖机2号正在售卖第1张票
售卖机2号正在售卖第2张票
售卖机1号正在售卖第1张票
售卖机3号正在售卖第1张票
售卖机1号正在售卖第4张票
售卖机2号正在售卖第3张票
售卖机1号正在售卖第6张票
结论:
我们会发现相同的票会出现很多次,这是因为当一个线程运行时,另一个进程可能也同步运行,他们先后顺序执行输出操作,未执行ticket--的操作,导致出现多次相同票。
多线程出现问题的环境:
那么我们如何解决多线程安全问题呢?
如何实现?
锁多条语句操作共享数据,可以通过同步代码块来实现:
synchronized(任意对象){
多条语句操作共享数据的代码
}
这里我们同样给出同步方法的格式:
//同步方法(这里的对象是 this)
public synchronized 返回类型 方法名(){
}
//同步静态方法(这里的对象是 类名.class)
public static synchronized 返回类型 方法名(){
}
同步的好处和弊端:
下面给出代码示例:(同步代码块)
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
Demo78.SellTicket st = new SellTicket();
//创建售卖机
Thread sell1 = new Thread(st,"售卖机1号");
Thread sell2 = new Thread(st,"售卖机2号");
Thread sell3 = new Thread(st,"售卖机3号");
//开始运行即可
sell1.start();
sell2.start();
sell3.start();
}
}
public class SellTicket implements Runnable{
//这里设置共有ticket
public static int ticket = 100;
//注意:这里需要创建private的Object对象,使所以线程共用一把锁
private Object obj = new Object();
//重构方法
@Override
public void run() {
//因为程序一直运行,需要在死循环里进行
while (true){
//采用同步代码块来实现锁方法
synchronized (obj){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
}
}
}
下面给出代码示例:(同步方法)
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
SellTicket st = new SellTicket();
//创建售卖机
Thread sell1 = new Thread(st,"售卖机1号");
Thread sell2 = new Thread(st,"售卖机2号");
Thread sell3 = new Thread(st,"售卖机3号");
//开始运行即可
sell1.start();
sell2.start();
sell3.start();
}
}
public class SellTicket implements Runnable{
//这里设置共有ticket
public static int ticket = 100;
private Object obj = new Object();
//重构方法
@Override
public void run() {
//因为程序一直运行,需要在死循环里进行
while (true){
//采用同步方法来实现锁方法
//这里可以采用SellMethod方法中任意一个
sellMethod3();
}
}
//sellMethod1:直接把内容带入方法中,在方法中使用同步代码块
private void sellMethod1() {
synchronized (obj){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
}
//sellMethod2:直接把方法变成同步方法(这里的锁是class本身,即this)
private synchronized void sellMethod2() {
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
//sellMethod3:直接把方法变成静态同步方法(这里的锁是class本身,即类名.class,即SellTicket.class)
private static synchronized void sellMethod3() {
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
}
}
我们给出一些线程安全的类,你可以在多线程的编译过程中直接使用以下类:
同步代码块和同步方法等同于Lock锁,但我们无法清晰看出锁的印记
于是Java给出Lock类清晰给出加锁和释放锁的方法
Lock是接口,不能直接实例化,所以我们采用它的实现类ReentrantLock来实例化
ReentrantLock构造方法:
下面给出代码示例:
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket对象
SellTicket st = new SellTicket();
//创建售卖机
Thread sell1 = new Thread(st,"售卖机1号");
Thread sell2 = new Thread(st,"售卖机2号");
Thread sell3 = new Thread(st,"售卖机3号");
//开始运行即可
sell1.start();
sell2.start();
sell3.start();
}
}
public class SellTicket implements Runnable{
//这里设置共有ticket
public static int ticket = 100;
//设置锁Lock(Lock是接口,所以需要采用继承类ReentrantLock来实现)
public Lock lock = new ReentrantLock();
//重构方法
@Override
public void run() {
//因为程序一直运行,需要在死循环里进行
while (true){
//这里锁上
lock.lock();
//当票还有时,售卖,ticket减一
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
ticket--;
}
//这里开锁
lock.unlock();
}
}
}
生产者消费者模式一个非常经典的多线程协作模式
实际上主要包含两类线程:
同时需要一个公共仓库
为了体现生产和消费的等待和唤醒状态,Java提供了Object类的一些方法:
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到其他线程采用唤醒方法(notify或notifyAll) |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
生产者消费者案例包含的类:
下面给出示例代码:
public class BoxDemo {
public static void main(String[] args) {
//创造一个奶盒
Box b = new Box();
//创造生产者对象,把奶盒当作公共资源放进去
Producer p = new Producer(b);
//创造顾客对象,把奶盒当作公共资源放进去
Customer c = new Customer(b);
//创造线程
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
//开始线程运行
t1.start();
t2.start();
}
}
public class Box {
//定义成员变量,表示第x瓶奶
private int milk;
//定义Box状态
private boolean state = false;
//提供存储牛奶的方法(要使用synchronized指定一次只能执行一个)
public synchronized void put(int milk){
//如果还有奶,暂时不需要存入,我们就将这个线程挂起
if (state){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//如果没有奶,我们需要把第x瓶奶放入
this.milk = milk;
System.out.println("送奶工将第" +this.milk + "瓶奶放入奶箱");
//然后我们修改奶瓶状态,使其他线程苏醒
state = true;
notifyAll();
}
//提供获得牛奶的方法(要使用synchronized指定一次只能执行一个)
public synchronized void get(){
//如果没有奶,我们需要等待,将这个线程挂起
if (!state){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//如果有奶,我们取奶
System.out.println("用户拿到第" + this.milk + "瓶奶");
//然后我们修改状态,并使其他线程苏醒
state = false;
notifyAll();
}
}
public class Producer implements Runnable{
private Box b;
public Producer(Box b) {
this.b = b;
}
@Override
public void run() {
//依次放入i瓶奶
for (int i = 1; i <= 5; i++) {
b.put(i);
}
}
}
public class Customer implements Runnable{
private Box b;
public Customer(Box b) {
this.b = b;
}
@Override
public void run() {
while (true){
b.get();
}
}
}
好的,关于Java相关的多线程我们就讲解到这里