Java自学之多线程编程

多线程编程是Java语言最为重要的特性之一。利用多线程技术可以提升单位时间内的程序处理性能,也是现代程序开发中高并发的主要设计模式。

进程与线程

进程是一个应用程序。线程是一个进程中的执行场景或者执行单元。一个进程可以启动多个线程。进程与进程之间内存独立不共享。同一个进程中的线程之间,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。

Java多线程实现

在Java中,如果要想实现多线程,那么就必须依靠一个线程的主体类,但是这个主体类在定义的时候也需要一些特殊的要求,这个类可以继承Thread类,实现Runnable接口或实现Callable接口来完成定义。

Thread类实现多线程

java.lang.Thread是一个负责线程操作的类,任何类只要继承了Thread类就可以成为一个线程的主类。同时线程类中需要明确覆写父类中的run()方法,当产生了若干个线程类对象时,这些对象就会并发执行run()方法中的代码。

class MyThread extends Thread {
     private String title;
     public MyThread(String title) {
         this.title = title;
     }
     @Override
     public void run(){
         for(int x; x < 10; x++) {
             System.out.println(this.title + '运行, x = ' + x);
         }
     }
 }

本程序定义了一个线程类MyThread,同时该类覆写了Thread类中的run()方法,在此方法中实现了信息的循环输出,虽然多线程的执行方法都在run()方法中定义,但是在实际进行多线程启动时并不能直接调用此方法,由于多线程需要并发执行,所以需要通过操作系统的资源调度才可以执行,这样对于多线程的启动就必须利用Thread类中start()方法完成,调用此方法时会间接调用run()方法。

class ThreadDemo {
     public static void main(String[] args) {
         new MyThread('线程A').start();
         new MyThread('线程B').start();
         new MyThread('线程C').start();
         new MyThread('线程D').start();
     }
 }

Runnable接口实现多线程

使用Thread类的确可以方便地实现多线程,但是这种方式最大的缺点就是单继承局限,为此在Java中也可以使用Runnable接口实现多线程。

class MyThread implements Runnable {
     private String title;
     public MyThread(String title) {
         this.title = title;
     }
     @Override
     public void run() {
         for (int x; x < 10; x++) {
             System.out.println(this.title + '运行,x = ' + x);
         }
 }
 }

利用Thread类定义的线程类可以直接在子类继承Thread类中所提供的start()方法进行多线程的启动,但是一旦实现了Runnable接口则MyThread类中将不再提供start()方法,所以为了继续使用Thread类启动多线程,此时就可以利用Thread类中构造方法进行线程对象的包裹。

class ThreadDemo {
     public static void main(String[] args) {
         Thread threadA = new Thread(new MyThread('线程对象A'));
         Thread threadB = new Thread(new MyThread('线程对象B'));
         Thread threadC = new Thread(new MyThread('线程对象C'));
         Thread threadD = new Thread(new MyThread('线程对象D'));
     }
 }

Callable接口实现多线程

使用Runnable接口实现的多线程可以避免单继承限制,但是Runnable接口实现的多线程会存在一个问题:Runnable接口里面的run()方法不能返回操作结果。所以为了解决这个问题,从JDK1.5开始对于多线程的实现提供了一个新的接口:java.util.concurrent.Callable。

Callable接口定义的时候可以设置一个泛型,此泛型的类型就是call()方法的返回数据类型,这样的好处是可以避免向下转型所带来的安全隐患。

class MyThread implements Callable {
     @Overrid
     public String call() throws Exception {
         for (int x; x < 10; x++) {
             System.out.println('线程执行,x = ' + x);
         }
         return 'www.uihtml.cn';
     }
 }

本程序利用Callable接口实现了一个多线程的主体类。并且在call()方法中定义了线程执行完毕后的返回结果。线程类定义完成后,如果要进行多线程的启动,依然需要同Thread类实现。所以此时可以通过java.util.concurrent.FutureTask类实现Callable接口与Thread类之间的联系,并且也可以利用FutureTask类获取Callable接口中call()方法的返回值。

class ThreadDemo {
     public static void main(String[] args) {
         startThread();
     }
     public static void startThread() throws Exception {
         FutureTask task = new FutureTask<>(new Thread());
         new Thread(task).start();
         System.out.println('线程返回数据:' + task.get());
     }
 }

多线程运行方法及常用的操作方法,请移步《JAVA学习之多线程知识点整理》

线程的同步与死锁

线程同步是指若干个线程对象并进行资源访问时实现的资源处理的保护操作。

package cn.uihtml.demo;
 class MyThread implements Runnable {
     private int ticket = 3;
     @Override
     public void run() {
         while(true) {
             if (this.ticket > 0) {
                 try {
                     Thread.sleep(100);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println(Thread.currentThread().getName() + "买票,ticket = " + this.ticket--);
             } else {
                 System.out.println('票已卖完了');
                 break;
             }
         }
     }
 }
package cn.uihtml.demo;
 class ThreadDemo {
     public static void main(String[] args) {
         MyThread mt = new MyThread();
         new Thread(mt,"售票员A").start();
         new Thread(mt,"售票员B").start();
         new Thread(mt,"售票员C").start();
     }
 }

假设现在只剩下最后一张票了,当第一个线程满足售票条件后(此时并未减少票数),其他的线程也有可能同时满足售票的条件,这样同时进行自减操作时就有可能造成负数。

造成并发资源访问不同步的主要原因在于没有将若干个程序逻辑单元进行整体性的锁定,即当判断数据和修改数据时只允许一个线程进行处理,而其他线程需要等待当前线程执行完毕后才可以继续执行,这样就使得在同一个时间段内,只允许一个线程执行操作,从而实现同步的处理。

Java中提供有synchronzied关键字以实现同步处理,同步的关键是要为代码加上“锁”,而对于锁的操作程序有两种:同步代码块、同步方法。

同步代码块是指使用synchronzied关键字定义的代码块,在该代码块执行时往往需要设置一个同步对象,由于线程操作的不确定状态,所以这个时候的同步对象可以选择this.

class MyThread impiements Runnable {
     private int ticket = 3;
     @Override
     public void run() {
         while(true) {
             synchronized(this) {
                 if (this.ticket > 0) {
                     try {
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     System.out.println(Thread.currentThread().getName() + "买票,ticket = " + this.ticket--);
                 } else {
                     System.out.println('票已卖完了');
                     break;
                 }
             }
         }
     }
 }
class ThreadDemo {
     public static void main(String[] args) {
         MyThread mt = new MyThread();
         new Thread(mt,"售票员A").start();
         new Thread(mt,"售票员B").start();
         new Thread(mt,"售票员C").start();
     }
 }

本程序将票数判断与票数自减的两个控制逻辑放在了一个同步代码块中,当进行多个线程并发执行时,只允许有一个线程执行此部分代码,就实现了同步处理操作。

同步代码块可以直接定义在某个方法中,使得方法的部分操作进行同步处理,但是如果现在某一个方法中的全部操作都需要进行同步处理,则可以采用同步方法的形式进行定义,即在方法声明上使用synchronized关键字即可。

class MyThread impiements Runnable {
     private int ticket = 3;
     @Override
     public void run() {
         while(this.sale()) {}
     }
     public synchronized boolean sale() {
         if (this.ticket > 0) {
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + "买票,ticket = " + this.ticket--);
             return true;
         } else {
             System.out.println('票已卖完了');
             return false;
         }
     }
 }
class ThreadDemo {
     public static void main(String[] args) {
         MyThread mt = new MyThread();
         new Thread(mt,"售票员A").start();
         new Thread(mt,"售票员B").start();
         new Thread(mt,"售票员C").start();
     }
 }

线程死锁

所谓的死锁,是指两个线程都在等待对方先完成,造成了程序的停滞状态。一般程序的死锁都是在程序运行时出现的。

综合案例

package cn.uihtml.demo;
 class Message {
     private String title;
     private String content;
     public void setTitle(String title) {
         this.content = content;
     }
     public String getTitle(){
         return this.title;
     }
     public void setContent(String content) {
         this.content = content;
     }
     public String getContent() {
         return this.content;
     }
 }
package cn.html.demo;
 class Producer implements Runnable {
     private Message msg = null;
     public Producer(Message msg) {
         this.msg = msg;
     }
     @Override
     public void run() {
         for (int x = 0; x < 50; x++) {
             if (x % 2 == 0) {
                 this.msg.setTitle("刘备");
             }
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             this.msg.setContent('蜀国国主');
         } else {
             this.msg.setTitle('曹操');
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             this.msg.setContent('魏国国主');
         }
     }
 }
package cn.uihtml.demo;
 class Consumer implements Runnable {
     private Message msg = null;
     public Consumer(Message msg) {
         this.msg = msg;
     }
     @Override
     public void run() {
         for (int x = 0; x < 50; x++) {             try {                 Thread.sleep(100)             } catch (InterruptedException e) {                 e.printStackTrace();             }             System.out.println(this.msg.getTitle() + '--->' + this.msg.getContent());
         }
     }
 }
package cn.uihtml.demo;
 class ThreadDemo {
     public static void main(String[] args) throws Exception {
         Message msg = new Message();
         new Thread(new Producer(msg).start());
         new Thread(new Consumer(msg).start());
     }
 }

本程序实现了一个基础的线程交互模型,但是通过执行结果可以发现程序中存在两个问题。

数据错位:假设生产者线程刚向数据存储空间添加了信息的名称,还没有加入这个信息的内容,程序就切换到了消费者线程,而消费者线程将把这个信息的名称和上一个信息的内容联系到了一起。

重复操作:生产者放了若干次的数据,消费者才开始取数据;或者是消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已取过的数据。

解决数据错位

通过分析可以知道引发数据错位问题的原因是由于数据不同步造成的。而数据同步的问题只能通过同步代码块或同步方法完成。

package cn.uihtml.demo;
 class Message {
     private String title;
     private String Content;
     public void setTitle(String title) {
         this.content = content;
     }
     public String getTitle(){
         return this.title;
     }
     public void setContent(String content) {
         this.content = content;
     }
     public String getContent() {
         return this.content;
     }
     public synchronized void set(String title,String content) {
         this.title = title;
         try {
             Thread.sleep(200)
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         this.content = content;
     }
     public synchronized String get() {
         try {
             Thread.sleep(100)
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         return this.title + '--->' + this.content;
     }
 }
package cn.uihtml.demo;
 class Producer implements Runnable {
     private Message msg = null;
     public Producer(Message msg) {
         this.msg = msg;
     }
     @Override
     public void run(){
         for(int x = 0; x < 50; x++) {
             if (x % 2 == 0) {
                 this.msg.set('刘备','蜀国国主');
             } else {
                 this.msg.set('曹操','魏国国主');
             }
         }
     }
 }
package cn.uihtml.demo;
 class Consumer implements Runnable {
     private Message msg = null;
     public Consumer(Message msg) {
         this.msg = msg;
     }
     @Override
     public void run(){
         for(int x = 0; x < 50; x++) {
             this.msg.get();
         }
     }
 }
package cn.uihtml.demo;
 class ThreadDemo {
     public static void main(String[] args) throws Exception {
         Message msg = new Message();
         new Thread(new Producer(msg).start());
         new Thread(new Consumer(msg).start());
     }
 }

解决重复操作问题

重复操作问题的解决需要引入线程的等待与唤醒机制,而这一机制的实现只能依靠Object类完成。

package cn.uihtml.demo;
 class Message {
     private String title;
     private String Content;
     private boolean flag = true; // true 允许生产,不允许消费;false 允许消费,不允许生产
     public void setTitle(String title) {
         this.content = content;
     }
     public String getTitle(){
         return this.title;
     }
     public void setContent(String content) {
         this.content = content;
     }
     public String getContent() {
         return this.content;
     }
     public synchronized void set(String title,String content) {
         if (this.flag == false) {
             try {
                 super.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
         this.title = title;
         try {
             Thread.sleep(100)
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         this.content = content;
         this.flag = false;    // 生产工作进行完毕,可以开始消费
         super.notify();    // 唤醒等待的线程
     }
     public synchronized String get() {
         if (this.flag == true) {
             try {
                 super.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
         try {
             Thread.sleep(10)
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         try {
             return this.title + '--->' + this.content;
         } finally {
             this.flag = true;    // 继续生产
             super.notify();    //  唤醒等待的线程
         }
     }
 }

本程序中追加了一个数据产生与消费操作的控制逻辑成员属性(flag),通过此属性的值控制实现线程的等待与唤醒处理操作,从而解决了线程重复操作的问题。

原创文章,作者:ZERO,如若转载,请注明出处:https://www.edu24.cn/course/java/java-multi-thread.html

Like (0)
Donate 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
ZEROZERO
Previous 2020年12月14日
Next 2020年12月21日

相关推荐

  • spring4.x学习之用户登录与注册

    在之前的文章中我已经把后端工程项目创建好了,接下来就是编写项目了。 首先,我先创建一个数据库。数据库使用的是MySQL,数据库管理工具用的是Navicat。 打开数据库管理工具Na…

    2019年3月21日
    1.8K
  • Java自学之泛型

    在Java语言中,为了方便接收参数类型的统一,提供了核心类Object,利用此类对象可以接收所有类型的数据(包括基本数据类型和引用数据类型)。但是由于其所描述的数据范围过大,所以在…

    2020年12月8日
    1.2K
  • Java自学之数组

    在Java中数组是一组相关变量的集合,属于引用数据类型。 定义 数组引用传递分析 数组属于引用数据类型,在数组使用时需要通过关键字new开辟堆内存空间,一块堆内存空间也可以同时被多…

    2020年11月26日
    1.1K
  • Spring Tools学习之JAVA开发环境搭建【windows 10】

    一直以来都想学习JAVA,但是苦于找不到门路,网上学习资料要不看不懂,要不需要金钱。 终于,迫于生活与年龄的压力,下定决心,学习JAVA。 写下此文档,记录一下自己学习JAVA之路…

    2018年12月6日
    2.7K
  • Java自学之类结构扩展

    面向对象中的核心组成是类与接口,在项目中会利用【包】进行一组相关类的管理,这样适合于程序代码的部分更新,也更加符合面向对象封装性的概念,同时合理地使用封装也可以方便地实现实例化对象…

    2020年12月10日
    1.2K
  • JAVA学习路线之夯实基础

    第一章 开发环境 JDK(Java SE Development Kit),Java标准版开发包,提供编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行环…

    2020年1月14日
    1.5K
  • JavaWeb入门案例之用户注册

    前言 之前把Java的基础知识系统的过了一遍,目前总算可以看懂Java代码了,接下来就是要学习Java开发框架(主要是springMVC)。 下面用一个用户注册的小案例,来总结一下…

    2021年1月13日
    1.4K
  • Spring Boot的常用注解

    未来的框架趋势是“约定大于配置”,代码的封装会更加严密。开发人员会将更多的精力放在代码的整体优化和业务逻辑上,所以注解式编程会被更加广泛地使用。那么什么是注解?Spring Boo…

    2024年8月29日
    261
  • Java自学之继承

    在面向对象的设计过程中,类是基本的逻辑单位。但是对于这些基本的逻辑单位需要考虑到重用的设计问题,所以在面向对象的设计里提供有继承,并利用这一特点实现类的可重用性定义。 类继承定义 …

    2020年12月3日
    1.2K
  • MyBatis之MyBatis-Generator标签配置及意义

    DTD 标签 <generatorConfiguration/>: 根标签,所有的配置都必须在该标签内配置;没有属性 <properties/>: 主要引用外部的pro…

    2019年12月27日
    1.7K

发表回复

Please Login to Comment