Java入门——(9)Path接口类,多线程总结


前言

继续学习一些接口类以及关于多线程的问题。


Path接口和Paths类

创建Path对象

可以通过Paths类的静态方法get()来创建一个Path对象,该方法接受一个字符串参数,表示文件或目录的路径。

例如:

import java.nio.file.Path;
import java.nio.file.Paths;

Path path = Paths.get("path/to/file.txt");

Path接口的方法

Path接口提供了许多方法来操作路径。以下是一些常用的方法:

  • Path getFileName(): 返回路径中的文件名部分。
  • Path getParent(): 返回路径中的父路径。
  • Path resolve(String other): 解析给定的路径字符串,并返回一个新的Path对象。
  • Path normalize(): 返回规范化的路径。
  • Path toAbsolutePath(): 返回绝对路径。
  • boolean startsWith(Path other): 判断当前路径是否以指定路径开始。
  • boolean endsWith(Path other): 判断当前路径是否以指定路径结束。
  • Path subpath(int beginIndex, int endIndex): 返回路径中指定索引范围的子路径。
  • boolean isAbsolute(): 判断路径是否为绝对路径。
import java.nio.file.Path;
import java.nio.file.Paths;

Path path = Paths.get("path/to/file.txt");
System.out.println("File Name: " + path.getFileName());
System.out.println("Parent Path: " + path.getParent());
System.out.println("Resolved Path: " + path.resolve("subdir/file2.txt"));
System.out.println("Normalized Path: " + path.normalize());
System.out.println("Absolute Path: " + path.toAbsolutePath());
System.out.println("Starts with \"path\": " + path.startsWith(Paths.get("path")));
System.out.println("Ends with \".txt\": " + path.endsWith(".txt"));
System.out.println("Subpath: " + path.subpath(0, 2));
System.out.println("Is Absolute: " + path.isAbsolute());

Files类

Files类的方法

Files类提供了一些常用的文件操作方法。以下是一些常用的方法:

  • boolean exists(Path path): 判断文件或目录是否存在。
  • boolean isDirectory(Path path): 判断路径是否表示一个目录。
  • boolean isRegularFile(Path path): 判断路径是否表示一个常规文件。
  • boolean isReadable(Path path): 判断文件或目录是否可读。
  • boolean isWritable(Path path): 判断文件或目录是否可写。
  • boolean isExecutable(Path path): 判断文件或目录是否可执行。
  • long size(Path path): 返回文件的大小。
  • boolean delete(Path path): 删除文件或目录。
  • void createDirectory(Path dir): 创建一个新的目录。
  • Path createFile(Path path, FileAttribute<?>... attrs): 创建一个新的文件。
  • List<String> readAllLines(Path path): 读取文件的所有行。
  • byte[] readAllBytes(Path path): 读取文件的所有字节。
  • void write(Path path, byte[] bytes, OpenOption... options): 将字节数组写入文件。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.io.IOException;
import java.nio.file.StandardOpenOption;

// 判断文件或目录是否存在
boolean exists = Files.exists(Paths.get("path/to/file.txt"));
// 判断路径是否表示一个目录
boolean isDirectory = Files.isDirectory(Paths.get("path/to/directory"));
// 判断路径是否表示一个常规文件
boolean isRegularFile = Files.isRegularFile(Paths.get("path/to/file.txt"));
// 判断文件或目录是否可读
boolean isReadable = Files.isReadable(Paths.get("path/to/file.txt"));
// 判断文件或目录是否可写
boolean isWritable = Files.isWritable(Paths.get("path/to/file.txt"));
// 判断文件或目录是否可执行
boolean isExecutable = Files.isExecutable(Paths.get("path/to/file.txt"));
// 返回文件的大小
long size = Files.size(Paths.get("path/to/file.txt"));
// 删除文件或目录
Files.delete(Paths.get("path/to/file.txt"));
// 创建一个新的目录
Files.createDirectory(Paths.get("path/to/newdir"));
// 创建一个新的文件
Path newFile = Files.createFile(Paths.get("path/to/newfile.txt"));
// 读取文件的所有行
List<String> lines = Files.readAllLines(Paths.get("path/to/file.txt"));
// 读取文件的所有字节
byte[] bytes = Files.readAllBytes(Paths.get("

IO流的基本知识

字节流

字节流主要用于处理二进制数据,可以实现从文件或网络中读取字节数据,或将字节数据写入文件或网络。Java提供了InputStreamOutputStream类以及它们的子类来实现字节流的操作。

  • InputStream:用于从输入源读取数据的基类。
  • OutputStream:用于向输出目标写入数据的基类。

处理IO异常

在文件不存在时,会出现IO异常

FileInputStream fis=null; 
try {
   fis=new FileInputStream("a.txt");
} catch(FileNotFoundException e) {
	//文件不存在异常
	System.*out*.println("File read error:"+e);
}

实例:

FileInputStream fis = null;
try {
    File f = new File("a.txt");
    // 创建字节流
    fis = new FileInputStream(f);
    // 根据文件的大小,创建相同长度的字节数组
    byte[] arr = new byte[(int) f.length()];
    // 读取流中的数据到字节数组中
    fis.read(arr);
    // 将读取的数据转换成String
    String msg = new String(arr);

    System.out.println(msg);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 判断字节流是否为空
    if (fis != null) {
        try {
            // 关闭字节流
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符流

字符流主要用于处理文本数据。Java提供了两个字符流类:Reader和Writer。与字节流类似,这两个类也是抽象类,我们通常使用它们的具体子类来进行操作。

  • Reader:用于从输入源读取字符数据的基类。
  • Writer:用于向输出目标写入字符数据的基类。

字符流在进行中文操作时会更加方便和准确,比如在读取中文文档内容时,字节流可能会读取到中文的一半,而字符流则不会出现这样的问题。

FileReader reader = null;
try {
    // 创建字符流
    reader = new FileReader("javakc.txt");
    // 创建字符数组,每次读取5个字符
    char[] arr = new char[5];
    // 读取的字符长度
    int length = 0;
    while ((length = reader.read(arr)) != -1) {
        // 将读取的数据转换成String
        String msg = new String(arr, 0, length);
        // 输出
        System.out.print(msg);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

进程与线程

几乎每种操作系统都支持进程的概念——进程就是在某种程度上相互隔离的、独立运行的程序,每一个进程都有自己独立的内存空间。比如IE浏览器程序,每打开一个IE浏览器窗口,就启动一个新的进程。在java中,我们执行java.exe程序,就启动一个独立的Java虚拟机进程,该进程的任务就是解析并执行Java程序代码。

线程是指进程中的一个执行流程,一个进程可以由多个线程组成,即一个进程中可以同时运行多个不同的线程,它们分别执行不同的任务。当进程内的多个线程同时运行时,这种运行方式成为并发运行。

线程又被称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统进行调度。

线程和进程的区别是:

  • 每个进程都有独立的代码存储空间(进程上下文),进程切换的开销大。

  • 线程没有独立的存储空间,而是和所属进程中其他的线程共享代码和存储空间,但每个线程有独立的运行栈程序计数器,因此线程切换的开销较小。

  • 多进程——在操作系统中能同时运行多个任务(程序),也称多任务。

  • 多线程——在同一应用程序中有多个顺序流同时执行。

线程的创建和启动

创建线程可以通过以下步骤进行:

  1. 导入相应的线程库(如threading
  2. 定义一个线程类,继承自Thread类或者实现Runnable接口
  3. 在线程类中重写run()方法,定义线程需要执行的任务
  4. 创建线程对象
  5. 调用线程对象的start()方法,启动线程
public class MyThread extends Thread {
	public void run(){
		//线程体内的实现
		for(int i=0;i<100;i++){
			System.out.println(i);
		}
	}
	
	public static void main(String[] args) {
		MyThread mt=new MyThread();
		mt.start(); //启动MyThread线程
	}
}

一个线程只能被启动一次,多次启动线程会抛出异常。

MyThread mt=new MyThread();
mt.start(); 
mt.start(); //抛出IllegalThreadStateException异常

接口实现:

public class MyThread implements Runnable {
	int count=0; 
	public void run(){
		while(true){
		 System.out.println(Thread.currentThread().getName()+":"+count++);
		 if(count>100){
			break;//当count大于100的时候,循环结束
		 }
		}
	}
	public static void main(String[] args) {
		MyThread mt=new MyThread();
		Thread t1=new Thread(mt);
		Thread t2=new Thread(mt);
		t1.start();
		t2.start();
	}
}

线程的状态转换

线程的状态可以包括以下几种:

  • 新建状态(New):用new语句创建的线程对象处于新建状态,此时和其他Java对象一样,仅仅在堆区中被分配了内存。

  • 就绪状态(Runnable):当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入了就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。

  • 运行状态(Running):处于这个状态的线程占用CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态。如果计算机有多个CPU,那么同一时刻可以让几个线程占用不同的CPU,使它们都处于运行状态。只有处于就绪状态的线程才有机会转到运行状态。

  • 阻塞状态(Blocked):

  • 阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,才有机会转到运行状态。

    阻塞状态可以分为以下3种:

    • 位于对象等待池中的阻塞状态:当线程处于运行状态时,如果执行了wait()方法,Java虚拟机就会把线程放到等待池中。
    • 位于对象锁池中的阻塞状态:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到锁池中。
    • 其他的阻塞状态:当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
  • 等待状态(Waiting):线程等待其他线程发送特定信号或条件满足

  • 超时等待状态(Timed Waiting):线程在等待一段时间后自动恢复

  • 死亡状态(Dead):当线程退出run(方法)时,就进入死亡状态,表示该线程结束生命周期。线程有可能是正常执行完run()方法而退出的,也有可能是遇到异常而退出。不管线程正常结束还是异常结束,都不会对其他线程造成影响。

  • 终止状态(Terminated):线程执行完毕或因异常终止

线程的调度

计算机通常只有一个CPU,在任何时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发,其实是指宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。在可运行池中,会有多个处于就绪状态的线程等待CPU,Java虚拟机的一项任务就是负责线程的调度。线程的调度是指按照特定的机制为多个线程分配CPU的使用权。

有两种调度模型:

  • 抢占式调度:优先级高的线程可以抢占CPU资源,优先执行任务
  • 协同式调度:线程执行一段时间后主动让出CPU资源,让其他线程执行
调整各个线程的优先级

所有处于就绪状态的线程根据优先级存放在可运行池中,优先级低的线程获得较少的运行机会,优先级高的线程获得较多的运行机会。Thread类的setPriority(int)getPriority()方法分别用来设置优先级和读取优先级。优先级用整数表示,取值范围是1~10,Thread类有以下3个静态常量。

  • MAX_PRIORITY:取值为10,表示最高优先级。

  • MIN_PRIORITY:取值为1,表示最低优先级。

  • NORM_PRIORITY:取值为5,表示默认的优先级。

线程睡眠:Thread.sleep()方法

当一个线程在运行过程中执行了sleep()方法时,它就会放弃CPU,转到阻塞状态。下面示例中每执行一次循环,就睡眠1000毫秒。

class MyThread extends Thread {
   int count = 0;
   public void run() {
   	while (true) {
   		System.out.print(getName() + ":" + count++);
   		try {
   			sleep(1000);//睡眠1秒钟
   		} catch (InterruptedException e) {
   			e.printStackTrace();
   		}
   	}
   }
}
class Client {
   public static void main(String[] arr) {
   	MyThread mt=new MyThread();
   	mt.start();
   }
}

Thread类的sleep(long millis)方法是静态的,millis参数设定睡眠的时间,以毫秒为单位。当执行sleep()方法时,就会放弃CPU开始睡眠,1秒钟后线程结束睡眠,就会获得CPU,继续进行下一次循环。所以会感觉程序运行很慢。

值得注意的是,当某线程结束睡眠后,首先转到就绪状态,假如其他的线程正在占用CPU,那么该线程就在可运行池中等待获得CPU。

线程让步:Thread.yield()方法

当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级的其他线程处于就绪状态,那么yield()方法将把当前运行的线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行线程,则yield()方法什么也不做。

下面代码中,线程执行完一次循环后,就执行了yield()方法。

class MyThread extends Thread {
   int count = 0;
   public void run() {
   	while (true) {
   		System.out.println(getName() + ":" + count++);
   		yield();
   	}
   }
}
class Client {
   public static void main(String[] arr) throws InterruptedException {
   	MyThread mt=new MyThread();
   	MyThread mt2=new MyThread();
   	mt.start();
   	mt2.start();
   }
}

从运行结果中,我们可以看到两个线程交替运行。

Thread-0:0

Thread-1:0

Thread-0:1

Thread-1:1

Thread-0:2

Thread-1:2

Thread-0:3

Thread-1:3

sleep()方法和yield()方法都是Thread类的静态方法,都会使当前处于运行状态的线程放弃CPU,把运行机会让给其他线程。两者的区别在于:

sleep()方法会给其他线程运行机会,而不考虑其他线程的优先级,因此会给较低优先级线程一个机会;yield()方法只会给相同优先级或者更高优先级线程一个运行的机会。

当线程执行了sleep(long millis)方法后,会转到阻塞状态,参数millis指定睡眠的时间;当线程执行了yield()方法后,将转到就绪状态。

sleep()方法方法抛出InterrupedException异常,而yield()方法没有声明抛出任何异常。

sleep()方法比yield()方法具有更好的可移植性。不能依靠yield()方法来提高程序的并发性能。对于大多数程序员来说,yield()方法的唯一用途是在测试期间人为地提高程序的并发性能,以帮助发现一些隐藏的错误。

等待其他线程结束:join()方法

当前运行的线程可以调用另一个线程的join()方法,当前运行的线程将转到阻塞状态,直至另一个线程运行结束,它才恢复运行。

class MyThread extends Thread {
	int count = 0;
	public void run() {
		while (count<5) {
			System.out.println(getName() + ":" + count++);
		}
	}
}
class Client {
	public static void main(String[] arr) throws InterruptedException {
		MyThread mt=new MyThread();
		mt.setName("mt");
		mt.start();
		mt.join();//主线程等待mt线程的运行结束
		System.out.println("main end");
	}
}

主线程调用了mt线程的join()方法,主线程将等到mt线程运行结束后,才能恢复运行。

mt:0

mt:1

mt:2

mt:3

mt:4

main end

join()方法有两种重载形式:

public void join()
public void join(long timeout)

timeout参数设定当前线程被阻塞的时间,以毫秒为单位。如果把示例main()方法中的mt.join()改为mt.join(10),那么当主线程被阻塞的时间超过了10毫秒,或者mt线程运行结束时,主线程就会恢复运行。

停止线程方法:stop()

stop这个方法是臭名昭著了,早就被弃用了,但是现在任然有很多钟情与他的人,永远都放不下他,因为从他的字面意思上我们可以知道他貌似可以停止一个线程,这个需求是每个搞线程开发的人都想要的操作,但是他并非是真正意义上的停止线程,而且停止线程还会引来一些其他的麻烦事,下面就来详细的介绍一下这个方法的:

从SUN的官方文档可以得知,调用Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:

1. 即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或finally语句中。

2. 会释放该线程所持有的所有的锁,而这种释放是不可控制的,非预期的.

当线程抛出ThreadDeath异常时,可以在该线程run()方法的任意一个执行点抛出,会导致该线程的run()方法突然返回来达到停止该线程的目的。

public class MyThread extends Thread{
	int count = 0;
	public void run() {
		while (true) {
			System.out.println(getName() + ":" + count++);
		}
	}
}

public class Test {
	public static void main(String[] args) throws InterruptedException {
		MyThread mt=new MyThread();
		mt.start();
		Thread.sleep(1000);
		mt.stop();//中断mt线程
		System.out.println("调用了stop方法");
	}
}

中断线程:interrupt()方法

线程的thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。

使用中断信号量中断非阻塞状态的线程

中断线程最好的,最受推荐的方式是,使用共享变量(shared variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地中止任务。下面代码描述了这一方式:

public class MyThread extends Thread{
	public void run(){
		for(int i=1; ;i++){
			if(Thread.interrupted()){
				break;
			}
			System.out.println(Thread.currentThread().getName()+":"+i);
		}	
	}
}
中断阻塞状态线程

Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,设置线程的中断标示位,在线程受到阻塞的地方(如调用sleep、wait、join等地方)抛出一个异常InterruptedException,并且中断状态也将被清除,这样线程就得以退出阻塞的状态。下面是具体实现:

public class MyThread extends Thread{
	public void run(){
		for(int i=1; ;i++){
			System.out.println(Thread.currentThread().getName()+":"+i);
			try {
				Thread.sleep(100000);
			} catch (InterruptedException e) {
				e.printStackTrace();
				break;
			}
		}	
	}
}

定时器

定时器用于在指定的时间间隔后触发某个动作或任务,常用的定时器包括timer模块和sched模块。

class Client {
	public static void main(String[] arr) throws InterruptedException {
		ScheduledExecutorService ses= Executors.newScheduledThreadPool(1);
		TimerTask task=new TimerTask(){
			public void run(){
				System.out.println("time is:"+new Date());
			}
		};
        //设置执行一次的定时任务
        ses.schedule(task,5000,TimeUnit.MILLISECONDS);	
        //设置反复执行,固定频率的定时任务
        ses.scheduleAtFixedRate(task,0,5000,TimeUnit.MILLISECONDS);
        //设置反复执行,固定间隔的定时任务
        ses.scheduleWithFixedDelay(task,0,5000,TimeUnit.MILLISECONDS);
	}
}

线程的同步

线程同步用于保证多个线程之间的协调和顺序执行,常见的线程同步机制有:

  • 锁(Lock):保证同一时间只有一个线程访问共享资源
  • 条件变量(Condition):用于线程间的等待和唤醒操作
  • 信号量(Semaphore):用于控制线程的并发数量
  • 事件(Event):线程间的事件通知机制,用于线程间的等待和通知

线程的职责就是执行一些操作,而多数操作都涉及到处理数据。下面的线程的操作主要是处理实例变量count。通过四个线程模拟卖票系统。

class TicketSell implements Runnable{
   //有100张票
   int count=100;
   public void run(){
   	while(true){
   		if(count>0){
   			try{
   				Thread.sleep(10);
   			}catch(Exception e){
   			}
   			System.out.println(Thread.currentThread().getName()                
                                                            +"="+count);
   			count--;
   		}else{
   			break;
   		}
   	}
   }
}
class Client {
   public static void main(String[] arr){
   	TicketSell ts=new TicketSell();
   	Thread t1=new Thread(ts);
   	Thread t2=new Thread(ts);
   	Thread t3=new Thread(ts);
   	Thread t4=new Thread(ts);
   	//启动四个线程卖票
   	t1.start();
   	t2.start();
   	t3.start();
   	t4.start();
   }
}

运行结果如下:

Thread-1=100

Thread-0=99

Thread-3=98

…………

Thread-0=3

Thread-3=2

Thread-2=1

Thread-1=0

Thread-3=-1

Thread-0=-1

票的数量count是一个共享资源,四个线程对共享资源开始了竞争。当线程Thread-2卖掉最后一张票后,其他的三个线程,还认为有最后一张票,继续卖票,造成了共享资源的并发问题。

以上卖票的操作被称为原子操作,一个线程在执行原子操作期间,应该采取措施使得其他线程不能操作共享资源,否则就会出现,共享资源被重复操作的问题。

同步代码块

为了保证每个线程能正常执行原子操作,Java引入了同步机制,具体做法是在代表原子操作的程序代码前加上synchronized标记,这样的代码被称为同步代码块。

class TicketSell implements Runnable{
	//有100张票
	int count=100;
	//同步锁
	Object o=new Object();
	public void run(){
		while(true){
			synchronized(o){
				if(count>0){
					try{
						Thread.sleep(10);
					}catch(Exception e){
					}
					System.out.println(Thread.currentThread().getName()+"="+count);
					count--;
				}else{
					break;
				}
			}
		}
	}
}

以上代码创建了Object对象o,在同步块中,o充当了同步锁的作用。

每个Java对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。当第一个线程拥有了这个同步锁,执行同步块里的代码时,其他的线程因为没有拥有这把锁,就不能执行同步块里的代码。即使该线程睡眠了,其他线程也是不能执行同步块里的代码。直到该线程执行完同步块释放了o的同步锁,其他线程才有机会执行同步块里的代码。

当前对象也可以作为同步锁使用,所以也可以这样写同步块:

synchronized(this){}

当一个线程开始执行同步代码块时,并不意味着以不中断的方式运行。进入同步代码的线程也可以执行Thread.sleep()或者执行Thread.yield()方法,此时它并没有释放锁,只是把运行机会让给了其他的线程

同步方法

使用synchronized关键字修饰的方法为同步方法,同步方法和同步块一样有线程同步的功能。

class TicketSell implements Runnable{
	//有100张票
	int count=100;
	//同步锁
	Object o=new Object();
	public void run(){
		while(true){
			if(count<1){
				break;
			}
			sale();
		}
	}
	public synchronized void sale(){
		if(count>0){
			try{
				Thread.sleep(10);
			}catch(Exception e){
			}
			System.out.println(Thread.currentThread().getName()+"="+count);
			count--;
		}
	}
}

同步方法中使用当前对象this作为同步锁,所以不需要额外声明同步锁。

synchronized声明不会被继承。如果一个用synchronized修饰的方法被子类覆盖,那么子类中这个方法不再保持同步,除非也用synchronized修饰。

同步与并发

同步是解决共享资源竞争的有效手段。当一个线程已经在操纵共享资源时,其他线程只能等待,只有当已经在操纵共享资源的线程执行同步代码后,其他线程才有机会操纵共享资源。

但是,多线程的同步与并发是一对此消彼长的矛盾。

假想有10个人同到一口井里打水,每个人都要打10桶水,人代表线程,井代表共享资源。一种同步方法是:所有的人依次打水,只用当前一个人打完10桶水后,其他人才有机会打水。当一个人在打水期间,其他人必须等待。轮到最后一个打水的人肯定怨声载道,因为他必须等到前面9个人打完90桶水后才能打水。

为了提高并发性能,应该使同步代码块中包含尽可能少的操作,使得一个线程能尽快释放锁,减少其他线程等待锁的时间。

线程安全的类

一个线程安全的类满足以下条件:

  • 这个类的对象可以同时被多个线程安全的访问。

  • 每个线程都能正常执行原子操作,得到正确的结果。

  • 在每个线程的原子操作都完成后,对象处于逻辑上合理的状态。

什么时候使用线程安全的类:

  • 程序中是否使用多线程

  • 如果使用了多线程:有没有临界资源

  • 如果有临界资源:考虑使用线程安全的类(数据是安全)

  • 如果有临界资源:自己写同步块或同步方法,保证数据安全

死锁

什么是死锁

死锁就是当两个或两个以上的线程因竞争相同资源而处于无限期的等待,这样就导致了多个线程的阻塞,出现程序无法正常运行和终止的情况。

死锁产生条件

死锁产生的4个必要条件:

  • 互斥条件:
    • 系统要求对所分配的资源进行排他性控制,即在一段时间内某个资源仅为一个进程所占有(比如:打印机,同一时间只能一个人打印)。此时若有其他进程请求该资源,则请求只能等待,直到有资源释放了位置;
  • 请求和保持条件:
    • 进程已经持有了一个资源,但是又要访问一个新的被其他进程占用的资源那么就会阻塞,并且对自己占用的一个资源保持不放;
  • 不剥夺条件:
    • 进程对已经获取的资源未使用完之前不能被剥夺,只能使用完之后自己释放。
  • 环路等待条件:
    • 存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
死锁代码
public class SyncDeadLock {

    private static Object objectA = new Object();
    private static Object objectB = new Object();

    public static void main(String[] args) {
        new SyncDeadLock().deadLock();
    }

    private void deadLock() {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (objectA) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " get objectA ing!");
                        Thread.sleep(500);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " need objectB! Just waiting!");
                    synchronized (objectB) {
                        System.out.println(Thread.currentThread().getName() + " get objectB ing!");
                    }
                }
            }
        }, "thread1");

        Thread thread2 = new Thread(new Runnable(){
            @Override
            public void run() {
                synchronized (objectB) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " get objectB ing!");
                        Thread.sleep(500);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " need objectA! Just waiting!");
                    synchronized (objectA) {
                        System.out.println(Thread.currentThread().getName() + " get objectA ing!");
                    }
                }
            }
        }, "thread2");

        thread1.start();
        thread2.start();
    }
}
如何避免死锁
  • 保持加锁顺序:

    • 当多个线程都需要加相同的几个锁的时候(例如上述情况一的死锁),按照不同的顺序枷锁那么就可能导致死锁产生,所以我们如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
  • 进行死锁检测:

    • 我们可以通过一些手段检查代码并预防其出现死锁。

总结

知识点描述
线程和进程的概念进程就是在某种程度上相互隔离的、独立运行的程序 线程是指进程中的一个执行流程,一个进程可以由多个线程组成
创建线程1、继承Thread类 2、实现Runnable接口 3、实现Callable接口
线程状态反应出线程是有生命的 新建-就绪-运行-阻塞 死亡
线程的方法sleep、yield、join、interrupt、setPrority、wait、notify
定时器线程池
线程的同步多线程并发执行,线程不安全:
1、同步块、同步方法
2、线程安全的类
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qiulaizhiwo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值