java-多线程(下)
多线程简单入门(Java)(下篇:多线程Java中的使用)
目录
一、创建多线程
二、线程的安全
三、线程的通信
一、创建多线程
在Java中,多线程的创建有4种方式。
方式一:继承于Thread类;
方式二:实现Runnable接口;
方式三:实现Callable()接口;
方式四:使用线程池。
方式一:继承于Thread类。步骤如下:
1、创建一个继承于Thread类的子类;
2、重写Thread类的子类的run()方法;
3、创建一个Thread类的对象;
4、通过对象调用start()方法开启线程。
代码:
//1、创建一个继承于Thread类的子类 class Mythread extends Thread{ //2、重写Thread类的run() @Override public void run() { super.run(); for (int i = 0; i < 100; i++) { if(i%2==0){ System.out.println(Thread.currentThread().getName()+":"+i);//Thread.currentThread().getName()是获取当前线程名的方法 } } } } public class ThreadTest{ public static void main(String[] args){ //3、创建Thread类的子类的对象 Mythread t1=new Mythread(); //4、通过此对象调用start():启动当前线程,调用当前线程的run()方法 t1.start(); } }
运行结果(打印了0到100的偶数):
方式二:实现Runnable接口。步骤如下:
(1)、创建一个实现Runnable接口的类;
(2)、(1)中的实现类去实现Runnable接口中的抽象方法run();
3)、创建实现类的对象;
(4)、创建Thread类的对象,并将(3)中的对象作为参数传递到Thread类的构造器中。
(5)、通过Thread类的对象调用start()方法。
代码:
//1、创建一个实现了Runnable接口的类 class Mthread implements Runnable{ //2、实现类去实现Runnable中的抽象方法:run() @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0){ System.out.println(Thread.currentThread().getName()+i); } } } } public class ThreadTest1 { public static void main(String[] args) { //3、创建实现类的对象 Mthread mthread = new Mthread(); // 4、此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread t1 = new Thread(mthread); //5、通过Thread类的对象调用start()://此时的run()是调用Mthread重写的run() t1.setName("线程一");//为线程设置名字 t1.start(); //再启动一个线程,也是遍历100以内的偶数 Thread t2 = new Thread(mthread); t2.setName("线程二"); t2.start(); } }
运行结果(线程一和线程二都会打印0-100的偶数):
方式一和方式二的比较:
开发中如何选择?优先选择Runnable接口的方式。原因有两个:1、实现Runnable接口的方式突破了Thread类的单继承性的局限性。2、实现Runnable接口的方式更适合处理有多个线程共享数据的情况。
二者的联系:其实Thread类也实现了Runnable接口:public class Thread implements Runnable。
二者的相同点:都需要实现run()方法,run()方法中声明了线程需要执行的逻辑。
下面我们举个例子简单说明一下Thread类中常用的方法。
1、start():启动当前线程并调用当前线程的run()方法。
2、run():通常需要重写Thread类中的此方法,将创建的线程执行的操作声明在此方法中。
3、currentThread():静态方法,返回当前代码执行的线程。
4、getName():获取当前线程的名字。
5、setName():设置当前线程的名字。
6、yield():释放当前CPU的执行权。也叫做线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同的或者更高优先级的线程。若等待队列中没有同优先级的线程,忽略此
方法。
7、join():当某个程序执行流中调用了其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。举个例子:
在线程A中调用线程B的join()方法,此时线程A进入阻塞状态。直到线程B完全执行完以后,线程A才结束阻塞状态。
8、stop():已过时。当执行此方法时,强制结束当前线程。
9、sleep(long millitime):让当前线程睡眠指定的毫秒数。此段时间内,当前线程是阻塞状态。在必要的时候执行sleep()方法会让线程执行地慢一些。
10、isAlive():判断当前线程是否还存活。
11、线程的优先级:
(1)、线程的优先级分为三个等级:最大优先级,最小优先级,默认优先级
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 默认优先级
2)、获取和设置当前线程的优先级
getPriority()
setPriority()
(3)、说明
高优先级的线程要抢占低优先级的线程,但是也只是从概率上这么说。高优先级的线程高概率地被执行,并不意味着”一定是高优先级先执行,结束后再执行低优先级“。
代码:
class HelloThread extends Thread{ @Override public void run() { super.run(); for (int i = 0; i < 100; i++) { //sleep()方法的测试:这个时候线程会执行地慢一些。 if (i % 2 == 0) { try { sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":" + i + "," + getPriority());//线程优先级默认是5 //yield()方法的测试:在线程执行到20的时候,必定会交出CPU执行权,让其他线程先执行一次。 // if (i % 20 == 0) { // Thread.currentThread().yield();//Thread.currentThread()相当于this. // } } } public HelloThread(String name) {//构造器 super(name); } public class ThreadMethodTest { public static void main(String[] args) throws InterruptedException { HelloThread h1=new HelloThread("Thread1");//本代码选择在构造器中设置线程的名字。 HelloThread h2=new HelloThread("Thread2");//本代码选择在构造器中设置线程的名字。 //设置名字是主线程所做的事。主线程这里指的是main()主线程 。 //h1.setName("线程一"); //设置线程的优先级。优先级的测试:主线程优先级最低,但是实际中,主线程也可能先执行。尤其说明,优先级高低,只是个概率事件。 h1.setPriority(Thread.MAX_PRIORITY); h1.start(); h2.setPriority(Thread.MAX_PRIORITY); h2.start(); //为主main()线程命名 Thread.currentThread().setName("主线程");//当前线程就是主线程 Thread.currentThread().setPriority(Thread.MIN_PRIORITY);//设置优先级 for (int i = 0; i <100 ; i++) { System.out.println(Thread.currentThread().getName()+":"+i+","+Thread.currentThread().getPriority()); //join()方法的测试:在主线程执行到20的时候,被阻塞,线程执行完了,主线程才继续执行。 // if(i==20){ // h1.join();//强行加入线程h1. // } } System.out.println(h1.isAlive());//测试线程是否执行完,执行完就是false.未执行完:true. } }
运行结果(每个人可能不一样,我这里是主线程先运行完,线程一线程二才运行的。主线程运行完,线程1的状态为true.):
方式三:实现Callable()接口(JDK5.0新增。该方法获取call()方法的返回值时需要借助FutureTask类)
1、创建一个实现Callable()接口的实现类。
2、实现Call方法,将此线程需要执行的操作声明在call()中。
3、创建实现Callable实现类的对象。
4、创建FutureTask类的对象,并将3中创建的实现类的对象作为参数传递到FutureTask的参数中。
5、将4中FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start();
6、获取Call()方法的返回值。
代码:
//1、创建一个实现Callable的实现类 class NumberThread implements Callable<Integer> { //2、实现Call方法,将此线程需要执行的操作声明在call()中。 @Override public Integer call() throws Exception { int sum=0; for (int i = 1; i <=100; i++) { if(i%2==0){ System.out.println(i); sum+=i; } } return sum;//自动装箱 } } public class ThreadNew { public static void main(String[] args) { //3、创建Callable接口实现类的对象 NumberThread numberThread=new NumberThread(); //4、将此Callable接口实现类的对象作为参数传递到FutureTask的构造器中。创建FutureTask的对象。 FutureTask<Integer> futureTask = new FutureTask<Integer>(numberThread); //5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread 对象,并调用start(); new Thread(futureTask).start();//futureTask实现了Runnable接口 //6、获取call方法的返回值。 Object sum = null; try {//get()方法的返回值即为futureTask构造器参数Callable实现类重写的call()的返回值。 sum = futureTask.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("总和为:"+sum); } }
运行结果(打印0-100的偶数,并返回0-100的所有偶数的和):
方式二和方式三的比较:
相比于run()方法,Callable()更加强大。call()方法可以有返回值;可以抛出异常;并支持泛型的返回值。
方式四:使用线程池
思路:经常创建和销毁,使用特别大的资源,比如并发情况下的线程,对性能影响很大。如果能提前创建好多个线程放入线程池中,在需要的时候直接获取,使用完再放回池中就很方
便了。这样能避免频繁地创建销毁线程,实现重复利用。
好处:1、能提高响应速度(减少了创建新线程的时间);
2、降低资源消耗(重复利用线程池,不需每次创建);
3、便于线程管理。
1、提供指定线程数量的线程池。
2、执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类的对象。Runnable()用service.execute()调用;Callable接口用service.submit()调用。
3、关闭连接池。
方法:ExecutorService,真正的线程池API.常见的子类:ThreadPoolExecutor。
void execute(Runnable command):执行任务/命令,没有返回值。一般用来执行Runnable。
void shutdown:关闭连接池。
Executors:工具类,线程池的工厂类。用于创建并返回不同类型的线程池。
Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程的线程池。
代码:
class NumberThreadPool implements Runnable{ @Override public void run() { for (int i = 0; i <= 100; i++) { if(i%2==0){ System.out.println(Thread.currentThread().getName()+"-"+i); } } } } class NumberThreadPool1 implements Runnable{ @Override public void run() { for (int i = 0; i <= 100; i++) { if(i%2==1){ System.out.println(Thread.currentThread().getName()+"-"+i); } } } } public class ThreadPool { public static void main(String[] args) { //1、提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); //设置线程池的属性 ThreadPoolExecutor service1=(ThreadPoolExecutor) service;//强转 System.out.println(service.getClass()); service1.setCorePoolSize(15); // service1.setKeepAliveTime(); //2、执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类的对象。 //service.submit(new NumberThreadPool());//适合使用于Callable() service.execute(new NumberThreadPool());//适合使用于Runnable()//打印偶数 service.execute(new NumberThreadPool1());//适合使用Runnable()//打印奇数 //关闭连接池 service.shutdown(); } }
运行结果:
二、线程的同步
先看一个窗口售票的例子:创建三个窗口卖票。总票数为100张
代码如下:
class Window extends Thread{ private static int ticket=100;//三个线程共享同一个ticket——总票数, // 三个线程卖100张票 @Override public void run() { super.run(); while(true){ if(ticket>0){ System.out.println(getName()+":卖票,票号为"+ticket); ticket--; }else{ break; } } } } public class WindowTest { public static void main(String[] args) { Window t1=new Window(); Window t2=new Window(); Window t3=new Window(); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } }
运行结果:出现了重票,存在线程安全问题。
分析一下,为何存在线程安全问题?---因为存在共享数据ticket,三个线程操作一个变量。
1、问题一:出现了重票错票问题。
2、问题二:出现的原因:当某个线程来操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作了车票
3、问题三:如何解决?当一个线程在操作ticket的时候,其他线程不能参与进来,直到线程A操作完以后,其他线程才可以操作。即使线程A出现了阻塞也不能被改变。
在Java中,我们通过同步机制来解决线程的安全问题。有如下三种方式。分别是同步代码块,同步方法,LOCK锁。有了同步的方式,操作代码时,只能有一个线程参与,其他线程等
待。相当于是一个单线程的过程。同步解决了线程的安全问题。
方式一:同步代码块 ,使用synchronized关键字。
synchronized(同步监视器){
//需要被同步的代码
}
说明:1、操作共享数据的代码,即为需要被同步的代码。不能包含多了,也不能包含少了。
2、共享数据:多个线程共同操作的变量,比如:ticket就是共享数据。
3、同步监视器:俗称:锁。任何一个类的对象都可以来充当这个锁。
注意:多个线程必须共用一把锁。在实现Runnable时,可以考虑用this充当这个锁。
代码:
//继承类的方法,同步监视器****慎用this class Window2 extends Thread { private static int ticket = 100;//三个线程共享同一个ticket——总票数, // 三个线程卖100张票 private static Object obj = new Object();//保证三个共享同一个数据 @Override public void run() { super.run(); while (true) {//Class class =Window2.class-> //synchronized (Window2.class) { synchronized (obj) {//此时不能用this,因为this代表着t1,t2,t3.此时锁不唯一。 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为" + ticket); ticket--; } else { break; } } } } } public class WindowTest2 { public static void main(String[] args) { Window2 t1=new Window2(); Window2 t2=new Window2(); Window2 t3=new Window2(); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } } //实现Runnable接口的方法 class Window1 implements Runnable { private int ticket = 100;//不用加static Object obj = new Object();//obj就是一个锁。 @Override public void run() { //Object obj = new Object();//obj只能是一个锁。唯一性 while (true) { synchronized (this) {//此时this代表的就是唯一的window1对象。 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket); ticket--; } else { break; } } } } } public class WindowTest1 { public static void main(String[] args) { Window1 w = new Window1();//共用一个window1对象 Thread t1 = new Thread(w);//w作为参数传递给线程Thread Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } }
运行结果(线程安全):
方式二:同步方法
如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明为同步的(使用synchronized关键字)。
关于同步方法的总结:
1、同步方法仍然涉及到同步监视器,只是不需要我们显式地声明。
2、非静态的同步方法,同步监视器是:this。
静态的同步方法,同步监视器是:当前类本身。
代码:
//使用同步方法来处理继承Thread类的方式中线程安全问题 class Window4 extends Thread { private static int ticket = 100;//三个线程共享同一个ticket——总票数, @Override public void run() { super.run(); while (true) { show(); } } private static synchronized void show(){//同步监视器:t1,t2,t2 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket); ticket--; } } } public class WindowTest4 { public static void main(String[] args) { Window4 t1=new Window4(); Window4 t2=new Window4(); Window4 t3=new Window4(); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } } //使用同步方法解决实现Runnable接口的线程安全问题 class Window3 implements Runnable { private int ticket = 100;//不用加static @Override public void run() { while (true) { show(); } } //同步方法 private synchronized void show(){ //synchronized(this){ if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket); ticket--; } } } public class WindowTest3 { public static void main(String[] args) { Window3 w = new Window3();//共用一个window1对象 Thread t1 = new Thread(w);//w作为参数传递给线程Thread Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } }
方式三:LOCK锁(JDK5.0新增)
方式一和方式二都使用到了synchronized关键字。那么,synchronized与LOCK有何异同?
相同:二者都可以解决线程安全的问题。
不同之处:synchronized机制在执行完相应的代码逻辑以后,自动地释放同步监视器。
另外需要注意的是LOCK需要手动地启动同步:lock(),同时结束也是需要手动的实现:unlock()。
实现LOCK锁步骤:
1、创建锁的对象;
2、上锁:调用lock()方法;
3、解锁:调用unlock()方法。
代码:
class Window implements Runnable{ private int ticket=100; //实例化lock private ReentrantLock lock=new ReentrantLock(true);//1、创建锁的对象 @Override public void run() { while(true){ try{ //2、调用lock方法 lock.lock(); if (ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"卖票:"+"票号为"+ticket); ticket--; }else{ break; } }finally{ //3、解锁的方法:unlock lock.unlock(); } } } } public class LockTest { public static void main(String[] args) { Window w=new Window(); Thread t1=new Thread(w); Thread t2=new Thread(w); Thread t3=new Thread(w); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); }
三、线程通信
下面以一个简单的例子来理解一下线程通信的方法。
//两个线程交替打印1到100 class Number implements Runnable{ private int number =1;//共享数据,线程安全问题 @Override public void run() { while(true){ synchronized (this) { //唤醒 notify();//notifyAll():唤醒多个。 if(number<=100){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+number); number++; //使得调用如下wait()方法的线程进入阻塞状态 try { wait();//wait完了以后会释放锁 } catch (InterruptedException e) { e.printStackTrace(); } } else { break; } } } } } public class CommunicationTest { public static void main(String[] args) { Number number = new Number(); Thread t1 = new Thread(number); Thread t2= new Thread(number); t1.setName("线程一"); t2.setName("线程二"); t1.start(); t2.start(); } }
运行结果:
线程通信有三个方法:wait(),notify(),notifyAll()。
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。wait()与sleep()不同,在被运用同步代码块中时,sleep()休眠以后,不会释放锁。但是wait()执
行完后会释放锁。
notify():一旦执行此方法,就会唤醒被wait()的一个线程。如果有多个线程,就会唤醒优先级高的线程。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明:
1、wait(),notify(),notifyAll()只能出现在同步块或同步方法中。
2、wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现MonitorStateException异常。
3、三个方法定义在Object类中。方便任何一个对象充当同步监视器的调用。
线程通信的应用:生产者消费者问题
分析:是多线程问题。生产者线程,消费者线程。
是否有共享数据?是,店员或产品的数量。
如何解决线程的安全问题?同步机制,三种方式。
代码:
class Clerk { private int productCount = 0;//生产的产品数量 //以下两个方法需要同步,否则存在线程安全问题 //生产产品(同步方法) public synchronized void produceProduct() { if (productCount < 20) { productCount++; System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品"); notify(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } //消费产品 public synchronized void consumerProduct() { if (productCount > 0) { System.out.println(Thread.currentThread().getName() + "开始消费第" + productCount + "个产品"); productCount--; notify(); } else {//等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread { private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { super.run(); System.out.println(Thread.currentThread().getName() + ":开始生产产品......."); while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } clerk.produceProduct();//生产 } } } class Consumer extends Thread { private Clerk clerk; public Consumer(Clerk clerk) { this.clerk=clerk; } @Override public void run() { super.run(); System.out.println(Thread.currentThread().getName() + ":开始消费产品......."); while (true) { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumerProduct();//消费 } } } public class ProduceTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer p1 = new Producer(clerk); p1.setName("生产者1"); Consumer c1 = new Consumer(clerk); c1.setName("消费者1"); Consumer c2= new Consumer(clerk); c2.setName("消费者2"); p1.start(); c1.start(); c2.start(); } }
运行结果:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)