首页 > 编程语言 >Java 中的线程

Java 中的线程

时间:2024-07-22 17:26:04浏览次数:16  
标签:Java Thread void static 线程 new public

创建线程的三种方式

方式一:继承Thread类

实现步骤:

  • 继承Thread类并重写run()方法;

  • 创建线程并启动。

代码实现:

public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i=0; i<100; i++) {
            System.out.println(i);
        }
    }
}

创建两个线程测试一下: 

public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.start();
        my2.start();
    }
}

 输出结果如下:

  • 为什么要重写run()方法?

    因为run()中封装被线程执行的代码。

方式二:实现Runnable接口

Thread构造方法

方法名说明
Thread(Runnable target)创建一个线程
Thread(Runnable target, String name)创建一个线程,线程名字为name

实现步骤:

  • 实现Runnable接口并重写run()方法,然后创建实现类的对象,表示线程要执行的任务;

  • 把Runnable接口实现类的对象作为构造方法的参数创建一个线程并启动;

代码实现:

public class RunDemo1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread() + "" + i);
        }
    }
}

创建线程对象:

public class TestDemo2 {
    public static void main(String[] args) {
        RunDemo1 r = new RunDemo1();
        // 其中r表示线程要执行的任务
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
    }
}
方式三:实现Callable接口

由于前两种方式的run方法都是没有返回值的,第三种可以获取返回值。

方法介绍:

方法名说明
V call()计算结果,如果无法计算结果,则抛出一个异常
FutureTask(Callable<V> callable)创建一个 FutureTask,一旦运行就执行给定的 Callable
V get()用来获取执行结果

实现步骤: 实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。

代码实现:

public class MyCallable implements Callable {

    @Override
    public Object call() throws Exception {
        return "nihao";
    }
}

测试一下: 

public class TestDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable cl = new MyCallable();
        FutureTask<String> ft = new FutureTask<String>(cl);
        Thread t = new Thread(ft);
        t.start();
        System.out.println(ft.get());
    }
}

输出结果:

线程中常用的方法

设置和获取线程名称的方法
方法名说明
void setName(String name)将线程的名字设置为参数name
String getName()返回此线程的名字
static Thread currentThread()返回执行到这行代码的线程
休眠线程方法
方法名说明
static void sleep(long millis)使当前正在执行的线程暂停指定的毫秒数

 需要注意的是,sleep 的时候要对异常进行处理。

try {//sleep会发生异常要显示处理
    Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
    e.printStackTrace();
}
线程优先级相关方法
方法名说明
final int getPriority()返回此线程的优先级
final void setPriority(int newPriority)更改线程的优先级,线程默认优先级是5

如果获取main线程的优先级呢?

首先使用Thread.currentThread()获取到main线程对象;

然后再使用getPriority()获取优先级。

Thread类中与优先级有关的代码,可以看到线程优先级的范围是1-10。

    // The minimum priority that a thread can have.
    public static final int MIN_PRIORITY = 1;

    // The default priority that is assigned to a thread.
    public static final int NORM_PRIORITY = 5;

    // The maximum priority that a thread can have.
    public static final int MAX_PRIORITY = 10;

数字越大表示优先级越高,cpu执行这个线程的概率越高,反之越低。

优先级只是概率问题,并不是绝对的。

下面举一个例子:

public class TestDemo1 {
    public static void main(String[] args) {
        ThreadDemo1 t1 = new ThreadDemo1("线程1:");
        ThreadDemo1 t2 = new ThreadDemo1("线程2:");

        t1.setPriority(1);
        t2.setPriority(10);

        t1.start();
        t2.start();
    }
}

程序的执行结果中是有可能优先级低的线程1先结束执行的。这里就不提供图片了。

守护线程方法
方法名说明
void setDaemon(boolean on)将此线程标记为守护线程,当非守护线程结束后守护线程也慢慢结束

 ThreadDemo1中的run方法:

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName() + ":" + i);
        }
    }

  ThreadDemo2中的run方法:

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName() + ":" + i);
        }
    }

 创建两个线程,然后将线程t2设置为守护线程,代码如下:

public class TestDemo4 {
    public static void main(String[] args) {
        ThreadDemo1 t1 = new ThreadDemo1();
        ThreadDemo2 t2 = new ThreadDemo2();
        // 将线程t2设置为守护线程
        t2.setDaemon(true);
        t1.start();
        t2.start();
    }
}

可以看到在非守护线程t1结束之后,守护线程t2也慢慢结束了。 

 

 礼让线程方法

Java 中的优先级不是特别的可靠,Java 程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法来决定的

插入线程方法

比如将线程t1插入到当前正在运行的main线程之前: 

public class TestDemo5 {
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo1 t1 = new ThreadDemo1();

        t1.start();
        t1.join();
        
        for (int i = 0; i < 40; i++) {
            System.out.println("main线程:" + i);
        }
    }
}

 运行结果:

Java 线程的 6 个状态:

// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

 线程的3个状态:就绪,运行,阻塞。图示如下:

cpu执行哪个线程是由操作系统进行调度的,有一些常见的线程调度算法,比如先来先服务,最短作业优先,最短剩余时间优先,轮转法等等。 

synchronized

同步代码块

多线程并发执行可能会出现数据安全问题,比如下面的三个窗口卖100张票的例子。

刚开始代码是这样写的:

public class MyThread1 extends Thread{
    static int ticket = 0;
    @Override
    public void run() {
        while (true) {
            if (ticket > 99) {
                break;
            } else {
                ticket++;
                System.out.println(getName() + "卖了第" + ticket + "张票");
            }
        }
    }
}

有两个问题:①相同的票;②超过范围的票。

下面是正确的代码,加锁的目的是为了使操作具有原子性,当锁没有释放时,别的线程是不能执行这段代码的,一般锁对象用static修饰保证唯一性。

public class MyThread1 extends Thread {
    static int ticket = 0;
    final static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 99) {
                    break;
                } else {
                    ticket++;
                    System.out.println(getName() + "卖了第" + ticket + "张票");
                }
            }
        }
    }
}

如果输出结果的顺序是混乱的,比如下面这种情况,是为什么呢?

因为锁没有起作用,比如定义创建一个锁的代码,因为在创建每一个新的线程的时候都会创建一个obj,锁不唯一。

final Object obj = new Object();
同步方法

用synchronized关键字修饰的方法。synchronized关键字写到访问修饰符的后面。

特点:

synchronized方法提供了一个锁,非静态方法的锁是调用此方法的对象,静态方法的锁是字节码文件。锁默认是打开的,当有一个线程进去之后,锁关闭,当代码执行完毕之后锁打开。

线程在哪里?

执行这行代码的线程。

尝试将上面例子中同步代码块的代码抽取为一个同步方法,代码如下,但是不对,因为同步方法中的锁是调用此方法的对象,此时有三个锁,分别是3个线程,锁不唯一。

public class MyThread1 extends Thread {
    static int ticket = 0;
    final static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            if (saleTicket()) break;
        }
    }

    private synchronized boolean saleTicket() {
        if (ticket > 99) {
            return true;
        } else {
            ticket++;
            System.out.println(getName() + "卖了第" + ticket + "张票");
        }
        return false;
    }
}

 所以同步方法一般使用Runnable接口,因为这种情况下锁唯一。

public class MyRunnable implements Runnable{
    static int ticket = 0;
    final static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            if (saleTicket()) break;
        }
    }

    private synchronized boolean saleTicket() {
        if (ticket > 99) {
            return true;
        } else {
            ticket++;
            System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
        }
        return false;
    }
}

Lock锁

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。Lock是接口,不能直接实例化,这里采用它的实现类ReentrantLock来实例化。

  • ReentrantLock构造方法

    方法名说明
    ReentrantLock()创建一个ReentrantLock的实例
  • 加锁解锁方法

    方法名说明
    void lock()获得锁
    void unlock()释放锁

代码如下,有两个细节:

①锁用static修饰,唯一性。

②当某个线程进入锁时,由于ticket < 100为false,执行else里的break结束while循环,但是并没有释放锁,所以程序无法结束,可以将lock.unlock();放到finally子句中确保锁一定会释放。 

有一个疑问:用synchronized的锁是什么释放的?

在大括号结束时释放的。

public class MyThread1 extends Thread {
    static int ticket = 0;
    final static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                if (ticket > 99) {
                    break;
                } else {
                    ticket++;
                    System.out.println(getName() + "卖了第" + ticket + "张票");
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

 子父类有关异常的处理?

生产者消费者模式(等待唤醒机制)

由于线程之间是抢占式执⾏的,因此线程之间执⾏的先后顺序难以预知。但是实际开发中希望协调多个线程的执行顺序,实现线程间的协作。

在 Java 中可以用 wait、notify 和 notifyAll 来实现线程间的通信。

wait和notify两个方法被提取到顶级父类Object中。Object类的等待和唤醒方法:

方法名说明
void wait()让当前线程进⼊等待状态,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify()随机唤醒等待队列中的一个线程
void notifyAll()唤醒等待队列中的所有线程

线程在运行的时候,如果发现某些条件没有被满足,可以调用wait方法放弃已经获得的锁,并且暂停自己的执行,然后进入等待状态。

当该线程被其他线程唤醒并获得锁后,可以沿着之前暂停的地方继续向后执行,而不是再次从同步代码块开始的地方开始执行。

但是需要注意的一点是,在线程通信中涉及到条件判断时要用while而不是if。因为while语句来说,这样在线程被唤醒后,会再次判断条件是否正真满足。

信号量实现
public class Food {
    static boolean flag;
    public synchronized static void eat() {
        while (!flag){
            try {
                Food.class.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("小张正在吃汉堡");
        flag = !flag;
        Food.class.notifyAll();
    }
    public synchronized static void cook() {
        while (flag){
            try {
                Food.class.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("小刘正在做法式大餐");
        flag = !flag;
        Food.class.notifyAll();
    }
}
public class Consumer extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            Food.eat();
        }
    }
}
public class Producer extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            Food.cook();
        }
    }
}
阻塞队列实现

即生产者和消费者中间有一个缓冲区或者叫容器用来存放产品,可以存放多个。

(1)当CPU执行到生产者这个线程时:

①首先判断容器里产品是否已满;

②已满就调用wait方法使自己进入等待状态;

③没有则生产,使产品数量+1,并唤醒等待的消费者进行消费。

关于其中的notifyAll:由于没有产品可消费了,消费者处于等待状态,当生产者生产了产品后,唤醒消费者。

(2)当CPU执行到消费者这个线程时:

①首先判断容器里是否有产品;

②没有就调用wait方法使自己进入等待状态;

③有则消费,使产品数量-1,并唤醒等待的生产者进行生产。

实现如下:

产品描述Product类代码如下:

public class Product {
    int id;

    public Product(int id) {
        this.id = id;
    }
}

 用来装产品的容器类SynContainer代码如下:

public class SynContainer {
    Product[] products = new Product[10];
    // 既表示产品的个数,也表示生产的产品存入数组中的下标
    int count = 0;

    public synchronized void put(Product p) {
        // 判断容器是否满了
        // 1. 满了
        while (count == products.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        products[count] = p;
        count++;
        this.notifyAll();
    }

    public synchronized Product take() {
        // 首先判断容器里是否有产品
        // 1. 没有
        if (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        count--;
        this.notifyAll();
        return products[count];
    }
}

生产者Producer的代码如下:

public class Producer extends Thread{
    SynContainer synContainer;

    public Producer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            synContainer.put(new Product(i));
            System.out.println("生产者生产了第" + i + "个产品");
        }
    }
}

消费者Consumer代码如下:

public class Consumer extends Thread{
    SynContainer synContainer;

    public Consumer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }
    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            System.out.println("消费者消费了第" + synContainer.take().id + "个产品");
        }
    }
}

测试类代码如下:

public class Test {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        Producer producer = new Producer(synContainer);
        Consumer consumer = new Consumer(synContainer);
        producer.start();
        consumer.start();
    }
}

由于线程中的run方法并没有使用锁,因此输出的时候顺序会乱,如下:

还有一个细节,如果如何确保生产者和消费者使用的是同一个容器,即在Producer和Consumer类中只声明一个SynContainer变量,由各自的构造方法传递实际的容器类对象。

标签:Java,Thread,void,static,线程,new,public
From: https://blog.csdn.net/m0_46977298/article/details/140572571

相关文章

  • JAVA值传递和引用传递
    值传递在调用方法时,将实参传递给了形参,但方法中无法通过改变形参直接改变实参。//值传递publicclassDemo{publicstaticvoidmain(String[]args){inta=1;System.out.println(a);//1Demo04.change(a);System.out.println(a);......
  • iOS开发-多线程编程
    OC中常用的多线程编程技术:1.NSThreadNSThread是Objective-C中最基本的线程抽象,它允许程序员直接管理线程的生命周期。NSThread*myThread=[[NSThreadalloc]initWithTarget:selfselector:@selector(myThreadMainMethod:)object:nil];[myThreadstart];使用NSThread时,......
  • 深入理解Java中的equals和hashCode方法
    序言:在Java编程中,equals和hashCode方法是两个非常重要的概念。它们直接关系到对象的比较和哈希表的使用效率。本文将详细介绍这两个方法的工作原理、如何正确重写它们以及一些常见的误区。一、equals方法equals方法的作用equals方法用于判断两个对象是否相等,返回一个布......
  • 中小公司的Java工程师应该如何成长
    文章来源:【非广告,纯干货】中小公司的Java工程师应该如何逆袭冲进BAT?【石杉的架构笔记】1、大部分人的情况1、在公司里的业务简单,都是用CRUD就能解决。2、用了用MQ、缓存、分库分表,但是也没什么并发量,数据量也不算特别大,成长缓慢。2、技术停滞的原因1、一部分是公司的技术框......
  • Python - Adob​​e InDesign Javascript 脚本帮助从 Python 调用 JSX
    提前致谢。希望每个人都表现出色。我试图从python调用Adob​​eIndesignJSX文件,下面是示例代码:我想在Adob​​eINdesign2024或更高版本上运行它。我在PythonInDesign脚本编写上看到了一些示例:从预检中获取溢出文本框以自动调整大小作为参考,可能适用于Ado......
  • JAVA数组
    数组概述数组是相同类型数据的有序集合,按一定的先后次序组合而成。每一个数据称为一个数组元素,每个数组元素可以通过一个下标来访问。数组声明创建定义数组变量类型变量名字=变量的值int[]nums;//首选intnums[];//次选//未赋值默认为空即int[]array=null;初始......
  • UOS系统部署KingbaseES V8R6 java故障“InvocationTargetException”
    案例说明:在UOS系统下部署KingbaseESV8R6数据库时,出现Java错误,部署失败。系统版本:kingbase@srv01:~$cat/etc/os-releasePRETTY_NAME="UnionTechOSServer20"NAME="UnionTechOSServer20"VERSION_ID="20"VERSION="20"ID=UOSHOME_URL="h......
  • Java语言程序设计基础篇_编程练习题**15.17 (几何问题:寻找边界矩形)
    **15.17(几何问題:寻找边界矩形)请编写一个程序,让用户可以在一个二维面板上动态地增加和移除点,如图15-29a所示。当点加入和移除的时候,一个最小的边界矩形更新显示。假设每个点的半径是10像素解题思路:这道题可以从编程练习题15.15修改新建一个面板Pane(),方法外部新建一个......
  • 【GeoJSON】Java 使用 GeoTools 将 SHP 文件转成 GeoJSON 文件
    文章目录引言Mavensettings.xml配置配置项目中的pom.xml引入GeoToolsJar包使用引言在使用GeoTools时,我们没办法直接使用Maven中央库的GeoTools,所以就需要我们配置一下关于GeoTools自己的镜像,所以我们才需要以下这几个步骤:1、检查一下自己本机maven的......
  • 科普文:TransmittableThreadLocal通过javaAgent实现线程传递并支持ForkJoin
    概叙TransmittableThreadLocal的介绍,直接看官网:https://github.com/alibaba/transmittable-thread-local目前使用中的稳定发布版本v2.x在 分支2.x上。帮助文档:https://github.com/alibaba/transmittable-thread-local#-%E5%8A%9F%E8%83%BD这篇文章主要介绍了Transmitta......