多线程编程是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