多线程一:多线程编程、多线程操作方法、线程同步与死锁 - Java高级特性1

目录

多线程编程

进程与线程

多线程的实现

继承Thread类实现多线程

Runnable实现多线程

 Thread与Runnable的关系

Callable实现多线程

多线程的运行状态

多线程常用操作方法

线程的命名与取得

线程休眠

线程中断

线程的强制运行

线程的礼让

线程的优先级

线程的同步与死锁

同步问题的提出

线程同步处理

线程死锁


学习笔记

多线程编程

在Java语言之中支持多线程开发,也是为数不多支持多线程的编程语言。在Java学习中,如果不能够对多线程的概念具有全面并且细致的了解,则在日后进行一些项目的设计过程之中,尤其是并发访问的设计之中就会出现严重的技术缺陷。

进程与线程

如果要想理解线程,那么必须需要了解进程的概念,在传统的DOS的时代,具有一个特征:如果你的电脑上出现了病毒那么所有的程序将无法进行,因为传统的DOS采用的是单进程的处理,而单进程处理的最大特点是:在同一个时间内,只允许一个进程的执行。那么后来到了Windos时代就开启了多进程的设计,表示在一个时间段上可以同时运行多个程序,并且这些程序轮流进行资源的抢占,所以在同一个时间段上会有多个程序一次执行,但是在同一个时间点上只有一个程序执行。而后来使用多核cpu,由于可以处理的cpu多了,那么即便有再多的进程出现,也可以比单核cpu处理提升。

线程是在进程的基础之上划分更小的程序单元,线程是在进程的基础上创建并使用的,所以线程依赖于进程的支持,线程的启动速度要比进程快许多,所以当使用多线程进行并发处理的时候,其执行的性能要高于进程。

Java是多线程的编程语言,所以Java在进行访问并发处理的时候可以得到更高的性能。

多线程的实现

如果现在要想在Java之中实现多线程的定义,那么就需要一个专门的线程主体类进行线程执行任务的定义,而这个主体类的定义是有要求的,必须实现实现特定的接口或继承特定的父类从才可以完成。

继承Thread类实现多线程

Java里面提供有一个Java.lang.Thread的程序类,那么一个类只要继承了此类就表示这个类为线程的主体类,但是并不是说这个类就能够实现多线程的处理了,还需要覆写Thread类中提供的run()方法(public void run​()),而这个方法就属于线程的主方法。

范例:多线程主体类

class MyThread extends Thread{  // 线程的主体类
	private String title ;
	public MyThread(String title) {
		this.title = title ;
	}
	@Override
	public void run() { // 线程的主体方法
		for ( int x = 0; x < 10; x++ ) {
			System.out.println(this.title + "运行.x = " + x);
		}
	}
	
}

多线程要执行的功能都应该在run()方法中进行定义。需要说明的是:在正常情况下要想使用一个类中的方法,那么肯定要产生实例化对象,而后去调用类中提供的方法,但是run()方法是不能够被直接调用的,因为这里面牵扯到操作系统的资源调度问题,所以要想启动多线程必须使用start()方法(public void start())完成。

范例:多线程启动

package cn.ren.demo;

class MyThread extends Thread{  // 线程的主体类
	private String title ;
	public MyThread(String title) {
		this.title = title ;
	}
	@Override
	public void run() { // 线程的主体方法
		for ( int x = 0; x < 10; x++ ) {
			System.out.println(this.title + "运行.x = " + x);
		}
	}	
}

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

通过此时调用你可以发现,虽然调用的是start()方法,但是最终执行的是run(),并且所有的线程对象都是交替执行的。执行顺序不可控,由start()控制。为什么不直接使用run(),而是必须使用start()方法?

如果要想清楚这个问题,需要查看一下start()方法的实现操作,通过源代码观察。

start()部分源代码:

public synchronized void start() {

    if (threadStatus != 0) // 判断线程的状态
        throw new IllegalThreadStateException(); // 抛出异常

    group.add(this);
    boolean started = false;
    try {
        start0(); // 在start()方法里面调用了start()方法
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {

        }
    }
}

private native void start0() ; // 只定义了方法名称,但是没有实现

发现在start()方法中会抛出一个“IllegalThreadStateException” 异常类对象,但是整个程序并没有使用throws或try...catch处理,因为该异常一定是RuntimeException的子类,每一个线程类的对象只允许启动一次,如果重复启动则抛出此异常,例如:下面的代码就会抛出异常。

public class ThreadDemo {
	public static void main(String[] args) {
		MyThread mt = new MyThread("线程A") ;
		mt.start();
		mt.start(); // 重复线程的启动

	}
}

出现异常:Exception in thread "main" java.lang.IllegalThreadStateException

在Java程序执行的过程中,考虑到不同层次的开发者的需求,所以其支持有本地的操作系统函数调用,而这项技术就被称为JNI(Java本地接口)技术,但是Java开发过程之中并不推荐这样使用,利用这项技术可以使用操作系统提供的底层函数进行一些特殊的处理,而在Thread类里面提供的start0()就表示需要将此方法依赖于不同的操作系统上实现。

任何情况下,只要定义了多线程,多线程的启动只有一种方案:Thread类中的start()。

Runnable实现多线程

虽然可以通过Thread类的继承实现多线程的定义,但是在Java程序之中对于继承永远都是存在单继承局限的,所以在Java中又提供有第二种多线程的主体定义结构形式:实现Java.lang.Runnable接口,此接口定义如下:

@FunctionalInterface  // 从JDK1.8引入Lambda表达式之后,就变为函数式的接口
public interface Runnable{
	public void run() ;
}

范例:通过Runnable实现多线程的主体类。

class MyThread implements Runnable {  // 线程的主体类
	private String title ;
	public MyThread(String title) {
		this.title = title ;
	}
	@Override
	public void run() { // 线程的主体方法
		for ( int x = 0; x < 10; x++ ) {
			System.out.println(this.title + "运行.x = " + x);
		}
	}	
}

此时由于不在继承Thread父类了,那么对于此时MyThread类中也就不在支持start()这个继承方法,可是如果不使用Thread.start()方法是无法进行多线程启动的,那么就需要观察一下Thread类提供的构造方法:

  • 构造方法:public Thread​(Runnable target); 

则将Runnable的子类对象传递到Thread的构造方法之中,这样也是可以访问Thread.start()的。

范例:启动多线程

public 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")) ;
		threadA.start() ; // 启动多线程
		threadB.start() ;
		threadC.start() ;
	}
}

发现这个多线程的实现里面,由于只是实现了Runnable的接口对象,所以此时线程主体类上就不再有单继承的局限,这样的设计才是标准性的设计。

可以发现从JDK1.8开始,Runnable接口使用了函数式接口定义,所以可以直接使用Lambda表达式实现线程类的定义。

范例:利用Lambda实现多线程的定义

public class ThreadDemo {
	public static void main(String[] args) {
		for (int x = 0; x < 3; x ++) {
			String title = "线程对象-" + x ;
			Runnable run = ()->{
				for (int y = 0; y < 10; y ++) {
					System.out.println(title + "运行.y = " + y );
				}
			} ;
			new Thread(run).start() ;
		}
	}
}

或者

public class ThreadDemo {
	public static void main(String[] args) {
		for (int x = 0; x < 3; x ++) {
			String title = "线程对象-" + x ;
			new Thread( ()->{
				for (int y = 0; y < 10; y ++) {
					System.out.println(title + "运行.y = " + y );
				}
			} ).start() ;
		}
	}
}

在以后的开发之中对于多线程的实现,优先考虑的Runnable接口实现,并且永远通过Thread类启动多线程。

 Thread与Runnable的关系

经过一系列的分析之后可以发现,在多线程的实现中已经有了两种做法:Thread类、Runnable接口,如果从代码的结构本身来讲,使用Runnable是最方便的因为可避免单继承的局限,同时能更好的进行功能的扩充。

但是从结构上观察Thread类与Runnable的关系,打开Thread定义:public class Thread extends Object implements Runnable。发现Thread类也是Runnable的子类,那么在之前继承Thread类的时候覆写的还是Runnable接口的run方法,于是此时来观察一下程序的类结构。

package cn.ren.demo;

class MyThread implements Runnable {  // 线程的主体类
	private String title ;
	public MyThread(String title) {
		this.title = title ;
	}
	@Override
	public void run() { // 线程的主体方法
		for ( int x = 0; x < 10; x++ ) {
			System.out.println(this.title + "运行.x = " + x);
		}
	}	
}

public 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")) ;
		threadA.start() ; // 启动多线程
		threadB.start() ;
		threadC.start() ;
	}
}

多线程的设计之中使用了代理设计模式的结构,用户自定义的线程只是负责项目核心功能的实现,而所有的辅助实现全部交由Thread类来处理。

在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但是通过Thread类的构造方法传递Runnable接口对象的时候,那么该接口对象将被Thread类中target属性所保存,在start()方法执行的时候会调用Thread类中的run()方法,而这个run()方法去调用Runnable接口子类被覆写过的run()方法。

多线程开发的本质是多个线程可以进行同一资源的抢占,那么Thread类主要描述的是线程,而资源的描述是Runnable完成的。

范例:利用买票程序来实现多个线程的资源并发访问

package cn.ren.demo;

class MyThread implements Runnable {  // 线程的主体类
	private int ticket = 5 ;

	@Override
	public void run() { // 线程的主体方法
		for ( int x = 0; x < 100; x++ ) {
			if (this.ticket > 0) {
				System.out.println( "买票.ticket = " + this.ticket -- );
			}
		}
	}	
}

public class ThreadDemo {
	public static void main(String[] args) {
		MyThread mt = new MyThread() ;
		new Thread(mt).start() ;   // 第一个线程启动
		new Thread(mt).start() ;   // 第二个线程启动
		new Thread(mt).start() ;   // 第三个线程启动
	}
}

通过内存分析图分析本程序的结构

Callable实现多线程

从传统的开发来讲如果要进行多线程是实现肯定依靠的就是Runnable,但是Runnable接口有一个缺点:但线程执行完毕后无法获取一个返回值,所以JDK1.5后就提出了一个新的线程接口:java.util.concurrent.Callable接口,首先观察这个接口的定义如下:

@FunctionalInterface
public interface Callable<V>{
    public V call() throws Exception ;
}

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

注:FutureTask与Callable通过构造方法联系:FutureTask​(Callable<V> callable)

范例:使用Callable实现多线程的处理

package cn.ren.demo;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyThread implements Callable<String> {
	@Override
	public String call() throws Exception {
		for (int i = 0; i < 10; i ++) {
			System.out.println("*******线程执行.x = " + i) ;
		}
		
		return "线程执行完毕";
	}
}

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		FutureTask<String> task = new FutureTask<String>(new MyThread()) ;
		new Thread(task).start() ; // 这里task向上两次转型,到Runnable,就可以传入Thread中
		System.out.println("【线程返回数据】" + task.get()) ;
	}
}

面试题:请解释Runnable与Callable的区别?

  • Runnable是在JDK1.0时提出的多线程接口,而Callable是在JDK1.5之后提出来的;
  • java.lang.Runnable接口之中只提供run()方法,并且没有返回值;
  • java.util.concurrent.Callable接口提供有call()方法可以有返回值;

多线程的运行状态

对于多线程的开发而言,编写程序的过程之中总是按照:定义线程主体类,通过线程的主体类进行启动,但是并不意味着调用了start()方法,线程就开始运行了,因为整体的线程处理有自己的一套运行状态。

1、任何的线程对象都应该使用Thread类进行封装,所以线程的启动使用的是start(),但是启动的时候实际上若干个线程都将进入到一种叫就绪状态,现在并没有执行。
2、进入到就绪状态之后,就需要等待进行资源的调度,当某一个线程调度成功之后则进入到运行状态(run()方法),但是所有的线程不可能一直持续的执行下去,中间需要产生一些暂停的状态,例如:某个线程执行一段时间时候,就需要让出资源,而后这个线程就进入到阻塞状态,随后重新回归到就绪状态;

3、当run()方法执行完毕之后,该线程的主要任务也就结束了,那么此时就可以直接进入到停止状态。

多线程常用操作方法

多线程的主要操作方法都在Thread类中定义。

线程的命名与取得

多线程的运行状态是不确定的,那么在程序开发之中为了可以获取一些需要使用的线程,就只能依靠线程的名字来进行操作。所以线程的名字是一个至关重要的概念,这样在Thread类之中就提供线程名称的处理:

  • 构造方法:public Thread​(Runnable target, String name) ;

  • 设置名字:public final void setName(String name) ;

  • 取得名字:public final String getName() ;

对于线程对象的获得是不可能只是依靠一个this来完成的,因为线程的状态不可控,所有的线程对象一定要执行run()方法,那么这个时候可以考虑获取当前线程,在Thread类中提供有获取当前线程的这个方法:

  • 获取当前线程: public static Thread currentThread​()

范例:观察线程的命名操作

package cn.ren.demo;

class MyThread implements Runnable{
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName());
	}
}

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		MyThread mt = new MyThread() ;
		new Thread(mt, "线程A").start(); ; // 设置了线程的名字
		new Thread(mt).start() ;		  // 未设置线程名字
		new Thread(mt, "线程B").start()  ; 
	}
}

当开发者为线程设置名字的时候就使用设置的名字,而如果没有设置名字,则会自动生成一个不重复的名字,这种自动的属性主要是依靠static属性完成的,在Thread类中定义有如下操作:

    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

范例:观察一个程序

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		MyThread mt = new MyThread() ;
		new Thread(mt, "线程对象").start(); ; // 设置了线程的名字
		mt.run(); // 对象直接调用run()方法
	}
}

通过此时的代码可以发现当使用了“mt.run()”中直接在主方法中调用线程类中对象中的run()方法,所获得的线程对象的名字为“main”,所以可以得出一个结论,主方法也是一个线程。那么现在的问题来了,所有的线程都是在进程上的划分,那么进程在哪里?每当使用Java命令启动的时候就表示启动了一个JVM的进程,一台电脑上可以启动若干个JVM进程,每个JVM的进程都会有各自的线程,进程之中的主方法就是主线程。

在任何的开发之中,主线程可以创建若干个子线程,创建子线程的目的是将一些复杂逻辑或者比较耗时的逻辑交由子线程处理;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		System.out.println("1、执行操作任务一");
		new Thread(()-> { // 子线程进行统计
			int  temp = 0 ;
			for (int i = 0; i < Integer.MAX_VALUE; i ++) {
				temp += i ;
			}			
		}).start() ; // 放在子线程中执行
		System.out.println("2、执行操作任务二");
		System.out.println("n、执行操作任务N");
	}
}

主线程负责处理流程,而子线程负责处理耗时操作。

线程休眠

如果说现在希望某一个线程可以暂缓执行,就可以使用休眠的处理,在Thread类中提供的方法如下:

在进行休眠的时候有可能会产生中断异常”InterruptedException“,中断异常属于Exception的子类,说明该异常必须进行处理。

范例:观察休眠处理

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		new Thread(()-> {
			for (int i = 0; i < 10; i++) {
				System.out.println(Thread.currentThread().getName() + "、i = " + i);
				try {
					Thread.sleep(1000); // 暂缓执行
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		} , "线程对象").start();
	}
}

休眠的主要特点可以自动实现线程的唤醒,以继续进行后续的处理,但是需要注意的是,现在你有多个线程对象,那么休眠也是有先后顺序的。

范例:观察产生多个线程对象的休眠处理

 

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Runnable run = ()-> {
			for (int i = 0; i < 10; i++) {
				System.out.println(Thread.currentThread().getName() + "、i = " + i);
				try {
					Thread.sleep(1000); // 暂缓执行
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		} ;
		for(int j = 0; j < 5 ; j ++) {
			new Thread(run, "线程对象-" + j).start();
		}
	}
}

此时产生五个线程对象,并且这五个线程对象执行的方法体是相同的。此时从程序执行的感觉,好像是若干个线程一起进行了休眠,而后一起进行了自动唤醒,但是实际上是有差别:所有对象一起进入到run()方法中,但是执行有先有后,进入之后线程的各个提示文字的有先有后输出,线程休眠也会有先有后执行。

线程中断

在之前线程的休眠里面,提供有中断异常,实际上线程的休眠是可以被打断的,而这种被打断肯定是由其它线程完成的,在Thread类中提供有这种中断执行的处理方法。

  • 判断线程是否被中断:public boolean isInterrupted​()

  • 中断线程执行:public void interrupt​()

范例:观察线程的中断处理操作

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Thread thread = new Thread(()->{
			System.out.println("***需要睡觉补充精力");
			try {
				Thread.sleep(10000) ;
				System.out.println("***睡足了,可以继续祸害");
			} catch (InterruptedException e) {			
				e.printStackTrace();
				System.out.println("打扰我睡觉,宰了你");
			} // 预计休眠10秒 
			
		}) ;
		thread.start() ; // 开始睡
		Thread.sleep(1000) ;
		if ( !(thread.isInterrupted()) ) { // 该线程没中断
			System.out.println("打扰睡眠");
			thread.interrupt(); // 中断执行
		}
	}
}

所有正在执行的线程都是可以被中断的,中断线程必须进行异常的处理。

线程的强制运行

所谓的线程的强制执行是:当满足于某些条件之后,某一个线程的对象将可以一直独占资源,一直到该线程的程序执行结束。

范例:观察一个没有强制执行的程序

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Thread thread = new Thread(()->{
			for (int x = 0; x < 100; x ++) {
				try {
					Thread.sleep(100) ;
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + ":执行.x=" + x);
			}
		}, "玩耍的线程");
		thread.start();
		for(int x = 0; x < 100; x++) {
			Thread.sleep(100) ;
			System.out.println("【霸道的main线程】number = " + x);
		}
	}
}

这个时候主线程和子线程都在交替执行着,但是如果你希望主线程独占执行就可以使用Thread类中的方法:

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Thread mainThread = Thread.currentThread(); // 获得主线程
		Thread thread = new Thread(()->{
			for (int x = 0; x < 100; x ++) {
				if (x == 3 ) { // 霸道的线程来了
					try {
						mainThread.join() ;
					} catch (InterruptedException e) {
						e.printStackTrace();
					} // 霸道线程先执行
					
				}
				try {
					Thread.sleep(100) ;
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + ":执行.x=" + x);
			}
		}, "玩耍的线程");
		thread.start();
		for(int x = 0; x < 100; x++) {
			Thread.sleep(100) ;
			System.out.println("【霸道的main线程】number = " + x);
		}
	}
}

在进行线程强制执行的时候一定要获取强制执行线程对象之后,才可以进行join()的调用。

线程的礼让

线程的礼让是:先将资源让出去让别的线程先执行。线程的礼让可以使用Thread中提供的方法:

  • 礼让:public static void yield​()

范例:使用礼让操作

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Thread thread = new Thread(()->{
			for (int x = 0; x < 100; x ++) {
				if (x % 3 == 0) {
					Thread.yield(); // 线程礼让
					System.out.println("##玩耍的线程礼让执行##	");
				}
				try {
					Thread.sleep(100) ;
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + ":执行.x=" + x);
			}
		}, "玩耍的线程");
		thread.start();
		for(int x = 0; x < 100; x++) {
			Thread.sleep(100) ;
			System.out.println("【霸道的main线程】number = " + x);
		}
	}
}

礼让执行的时候每一次调用yield()方法都只会礼让一次当前的资源。

线程的优先级

从理论上来讲,线程的优先级越高越有可能先执行(越有可能抢占到资源),在Thread中针对优先级提供了两个方法:

  • 设置优先级:public final void setPriority​(int newPriority)
  • 获取优先级:public final int getPriority​()

在进行优先级定义的时候通过int型数据完成的,而对于此数字的选择在Thread类中定义有三个常量:

  • 最低优先级:public static final int MIN_PRIORITY、1

  • 中等优先级:public static final int NORM_PRIORITY、5

  • 最高优先级:public static final int MAX_PRIORITY、10

范例:观察优先级

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Runnable run = () -> {
			for (int x = 0; x < 10; x ++ ) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "执行");
			}
		} ;
		Thread threadA = new Thread(run, "线程对象A") ;
		Thread threadB = new Thread(run, "线程对象B") ;
		Thread threadC = new Thread(run, "线程对象C") ;
		threadA.setPriority(Thread.MIN_PRIORITY) ;
		threadB.setPriority(Thread.MIN_PRIORITY) ;
		threadC.setPriority(Thread.MAX_PRIORITY) ;
		threadA.start();
		threadB.start();
		threadC.start();
	}
}

主方法是一个主线程,那么主线程的优先级?

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		System.out.println(new Thread().getPriority());
		System.out.println(Thread.currentThread().getPriority());
	}
}

主线程是属于中等优先级,而默认创建的线程也是中等优先级的。

线程的同步与死锁

在多线程的处理之中,可以使用Runnable描述多个线程的资源,而Thread描述每一个线程对象,于是当多个线程访问同一个资源的时候,如果处理不当就会产生数据错误操作。

同步问题的提出

下面编写一个简单的买票程序,将创建若干个处理对象实现买票的操作。

范例:实现买票操作

package cn.ren.demo;

class MyThread implements Runnable {
	private int ticket = 10 ; // 总票数为10张
	@Override
	public void run() {
		while(true) {
			if(this.ticket > 0) {
				System.out.println(Thread.currentThread().getName() + "买票.ticket=" + this.ticket --) ;
			} else {
				System.out.println("票已经卖光");
				break ;
			}
		}
		
	}
}

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		MyThread mt = new MyThread() ;
		new Thread(mt,"票贩子A").start() ;
		new Thread(mt,"票贩子B").start() ;
		new Thread(mt,"票贩子C").start() ;

	}
}

此时创建三个线程,并且这三个线程将进行五张票的出售。此时程序在进行买票处理的时候并没有任何的问题(这是假象),下面可以模拟一下买票中延迟操作。

package cn.ren.demo;

class MyThread implements Runnable {
	private int ticket = 10 ; // 总票数为10张
	@Override
	public void run() {
		while(true) {
			if(this.ticket > 0) {
				try {
					Thread.sleep(1000) ; // 模拟网络延迟
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "买票.ticket=" + this.ticket --) ;
			} else {
				System.out.println("****票已经卖光****");
				break ;
			}
		}
		
	}
}

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		MyThread mt = new MyThread() ;
		new Thread(mt,"票贩子A").start() ;
		new Thread(mt,"票贩子B").start() ;
		new Thread(mt,"票贩子C").start() ;

	}
}

这个时候追加了延迟问题就暴露出来了,而实际上这个问题一直都在。分析如下:

 

A线程在休眠的时候,票数未改变,B、C线程依然可以通过判断,则导致线程不同步问题。

线程同步处理

经过分析之后已经可以确认同步问题所产生的主要原因,下面将要进行同步问题的解决,但是解决同步问题的关键是锁,即当某个线程执行操作的时候,其它线程外面等待。

如果要想在程序之中实现这把锁的功能,就需要使用synchronized关键字来实现,利用此关键字可以定义同步方法或同步代码块,在同步代码块的操作里面的代码只允许一个线程执行。

1、利用同步代码块进行处理

synchronized  (同步对象) {
    同步代码操作 ;
}

一般进行同步处理的时候,选择当前对象this进行同步。

范例:利用同步代码块解决数据的同步问题

package cn.ren.demo;

class MyThread implements Runnable {
	private int ticket = 1000 ; // 总票数为10张
	@Override
	public void run() {
		while(true) {
			synchronized (this) { // 每一次只允许一个线程进行访问
				if(this.ticket > 0) {
					try {
						Thread.sleep(10) ; // 模拟网络延迟
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "买票.ticket=" + this.ticket --) ;
				} else {
					System.out.println("****票已经卖光****");
					break ;
				}
			}
		}
	}
}

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		MyThread mt = new MyThread() ;
		new Thread(mt,"票贩子A").start() ;
		new Thread(mt,"票贩子B").start() ;
		new Thread(mt,"票贩子C").start() ;

	}
}

上面已经解决了同步问题,但是加入同步处理之后程序的执行性能下降了,同步实际上会造成性能的降低。

2、利用同步方法解决:只需要在方法上加入synchronized关键字即可。

package cn.ren.demo;

class MyThread implements Runnable {
	private int ticket = 100 ; // 总票数为10张
	public synchronized boolean sale() {
		if(this.ticket > 0) {
			try {
				Thread.sleep(10) ; // 模拟网络延迟
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "买票.ticket=" + this.ticket --) ;
			return true ;
		} else {
			System.out.println("****票已经卖光****");
			return false ;
		}
	}
	@Override
	public void run() {
		while(this.sale()) { 
			;
		}
	}
}

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		MyThread mt = new MyThread() ;
		new Thread(mt,"票贩子A").start() ;
		new Thread(mt,"票贩子B").start() ;
		new Thread(mt,"票贩子C").start() ;

	}
}

在日后学习Java类库的时候会发现,系统中许多类上使用的同步处理采用的都是同步方法。

线程死锁

死锁是在进行多线程同步的处理之中有可能产生的问题,死锁指的是若干个线程彼此等待的状态。下面通过一段代码简单观察一下死锁的表现形式,但是此代码不是重点。

范例:死锁的展示

package cn.ren.demo;

public class DeadLock implements Runnable {
	private PersonOne po = new PersonOne() ;
	private PersonTwo pt = new PersonTwo() ;
	
	@Override
	public void run() {
		po.say(pt);
				
	}
	public DeadLock() {
		new Thread(this).start() ;
		pt.say(po);
	}
	public static void main(String[] args) {
		new DeadLock() ;
	}
}

class PersonOne {
	public synchronized void say(PersonTwo pt) {
		System.out.println("此路是我开,要想从此过,留下买路财");
		pt.get();
	}
	public synchronized void get() {
		System.out.println("PersonOne: 得到了财,让出路");
	}
}
class PersonTwo {
	public synchronized void say(PersonOne po) {
		System.out.println("我先过,再给钱。");
		po.get();
	}
	public synchronized void get() {
		System.out.println("PersonTwo: 过了路");
	}
}



现在死锁造成的主要原因是彼此等待着,等待对方先让出资源。死锁实际上是开发中出现的不确定的状态,有的时候代码处理不当则会不定期出现死锁,这是属于正常开发中的调试问题。

若干个线程访问同一资源的时候一定要进行同步处理,而过多容易造成死锁。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值