1.对象锁和类锁是否会互相影响?
· 对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
· 类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。
· 类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的
2.线程池是什么?Java四种线程池的使用介绍
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
Java四种线程池的使用:
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
3.谈谈你对多线程同步机制的理解?
线程同步是为了确保线程安全,所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。(如果多线程程序运行结果和单线程运行的结果是一样的,且相关变量的值与预期值一样,则是线程安全的。
4.多线程读写同一个文件有哪些场景需要同步处理?
有线程正在读文件,另开辟线程写文件;
有线程正在写文件,另开辟线程读文件;
有线程正在写文件,另开辟线程写文件
总之,读写互斥,写读互斥,写写互斥,只有读读相容(可以异步)。
使用对文件加锁的方式做到线程安全
FileInputStream、FileOutputStream、RandomAccessFile均可得到FileChannel对象,对文件锁进行操作。
独占锁tryLock()
FileChannel的tryLock()是非阻塞的,也就是说,在发现文件被锁住的时候,直接返回null,并且抛出异常,如果没有锁住,直接返回该文件的文件锁。
它是独占锁,就是只能被一个线程持有,它能禁止其他线程获取共享锁,可用于写文件。
while (true) {
try {
fileLock = fileChannel.tryLock();//独占锁
break;
} catch (Exception e) {
System.out.println("有其他线程正在操作该文件,当前线程" + Thread.currentThread().getName());
}
}
共享锁tryLock(0, Long.MAX_VALUE, true)
FileChannel的tryLock(0, Long.MAX_VALUE, true)是非阻塞的,在发现文件被锁住的时候,直接返回null,并且抛出异常,如果没有锁住,直接返回该文件的文件锁。
它是共享锁,能被多个线程同时持有,它能禁止其他线程获取独占锁,可用于读文件。
while (true) {
try {
fileLock = fileChannel.tryLock(0, Long.MAX_VALUE, true);//共享锁
break;
} catch (Exception e) {
System.out.println("有其他线程正在操作该文件,当前线程" + Thread.currentThread().getName());
}
}
独占锁lock()
而FileChannel的lock()是阻塞的,在文件被锁定的情况下,会保持阻塞,直到获得该锁为止。
fileLock = fileChannel.lock();
5.一、多线程下载原理
多线程下载文件时,文件是被分成多个部分,是被不同的线程同时下载的,此时就需要每一条线程都分别需要一个记录点,和每个线程完成状态的记录。只有将所有线程的下载状态都出于完成状态时,才能表示文件下载完成。
二、断点续传原理
1、从字面意义上理解:
断点:线程停止的位置
续传:从停止的位置继续下载
2、从代码上理解:
断点:当前线程已经下载完成的数据长度
续传:向服务器请求上次线程停止的位置之后的数据
3、总原理:
每当线程停止时就把已经下载的数据长度写入记录文件,这段长度就是所需要的断点,当重新下载时,从记录的文件位置,通过设置不同的网络请求参数,向服务器请求上次线程停止的位置之后的数据。
4、文件下载
其实就是IO的读写,通过HttpUrlConnection进行网络请求,IO流读取文件并且慢慢下载到本地文件,期中,可以、利用HttpUrlConnection的setRequestProperty(String filed,String newValue)方法可以实现需要下载文件的一个范围。setRequestProperty("Range","bytes="+开始位置+"-"+结束位置) 利用这个方法时,他只能实现续传的一部分需求,并不能提供从指定的位置写入数据的功能,这时候就需要使用RomAccessFile来实现从指定位置给文件写入数据的功能。
三、多线程下载及其断点续传实现原理
1、首先获取要下载文件的长度,用来设置RomdomAccessFile(本地文件)的长度
2、实时保存文件的下载进度(此功能可以用数据库来实现)
3、中断后再次下载,读取进度,再从上次的下载进度继续下载,并在本地的文件续续写如。
获取文件长度:fileLength = HttpUrlConnection.getContentLength()
每条线程需要下载的大小 = fileLength / Thread_Num
6. 断点续传
我们要实现的效果很简单:将在D盘的”test.txt”文件写入到E盘当中,但中途我们会模拟一次”中断”行为,然后在重新继续上传,最终完成整个过程。
也就是说,我们这里将会把“D盘”视作一台电脑,并且直接将”E盘”视作一台服务器。那么这样我们甚至都不再与http协议扯上半毛钱关系了,(当然实际开发我们肯定是还是得与它扯上关系的 <),从而只关心最基本的文件读写的”断”和”续”的原理是怎么样的。
为了通过对比加深理解,我们先来写一段正常的代码,即正常读写,不发生中断:
public class Test { public static void main(String[] args) { // 源文件与目标文件
File sourceFile = new File("D:/", "test.txt");
File targetFile = new File("E:/", "test.txt"); // 输入输出流
FileInputStream fis = null;
FileOutputStream fos = null; // 数据缓冲区
byte[] buf = new byte[1]; try {
fis = new FileInputStream(sourceFile);
fos = new FileOutputStream(targetFile); // 数据读写
while (fis.read(buf) != -1) {
System.out.println("write data...");
fos.write(buf);
}
} catch (FileNotFoundException e) {
System.out.println("指定文件不存在");
} catch (IOException e) { // TODO: handle exception
} finally { try { // 关闭输入输出流
if (fis != null)
fis.close(); if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
该段代码运行,我们就会发现在E盘中已经成功拷贝了一份“test.txt”。这段代码很简单,唯一稍微说一下就是:
我们看到我们将buf,即缓冲区 设置的大小是1,这其实就代表我们每次read,是读取一个字节的数据(即1个英文字母)。
现在,我们就来模拟这个读写中断的行为,我们将之前的代码完善如下:
public class Test { private static int position = -1; public static void main(String[] args) { // 源文件与目标文件
File sourceFile = new File("D:/", "test.txt");
File targetFile = new File("E:/", "test.txt"); // 输入输出流
FileInputStream fis = null;
FileOutputStream fos = null; // 数据缓冲区
byte[] buf = new byte[1]; try {
fis = new FileInputStream(sourceFile);
fos = new FileOutputStream(targetFile); // 数据读写
while (fis.read(buf) != -1) {
fos.write(buf); // 当已经上传了3字节的文件内容时,网络中断了,抛出异常
if (targetFile.length() == 3) {
position = 3; throw new FileAccessException();
}
}
} catch (FileAccessException e) {
keepGoing(sourceFile,targetFile, position);
} catch (FileNotFoundException e) {
System.out.println("指定文件不存在");
} catch (IOException e) { // TODO: handle exception
} finally { try { // 关闭输入输出流
if (fis != null)
fis.close(); if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} private static void keepGoing(File source,File target, int position) { try {
Thread.sleep(10000);
} catch (InterruptedException e) { // TODO Auto-generated catch block
e.printStackTrace();
} try {
RandomAccessFile readFile = new RandomAccessFile(source, "rw");
RandomAccessFile writeFile = new RandomAccessFile(target, "rw");
readFile.seek(position);
writeFile.seek(position); // 数据缓冲区
byte[] buf = new byte[1]; // 数据读写
while (readFile.read(buf) != -1) {
writeFile.write(buf);
}
} catch (FileNotFoundException e) { // TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) { // TODO Auto-generated catch block
e.printStackTrace();
}
}
} class FileAccessException extends Exception {
}
总结一下,我们在这次改动当中都做了什么工作:
- 首先,我们定义了一个变量position,记录在发生中断的时候,已完成读写的位置。(这是为了方便,实际来说肯定应该讲这个值存到文件或者数据库等进行持久化)
- 然后在文件读写的while循环中,我们去模拟一个中断行为的发生。这里是当targetFile的文件长度为3个字节则模拟抛出一个我们自定义的异常。(我们可以想象为实际下载中,已经上传(下载)了”x”个字节的内容,这个时候网络中断了,那么我们就在网络中断抛出的异常中将”x”记录下来)。
- 剩下的就如果我们之前说的一样,在“续传”行为开始后,通过RandomAccessFile类来包装我们的文件,然后通过seek将指针指定到之前发生中断的位置进行读写就搞定了。
(实际的文件下载上传,我们当然需要将保存的中断值上传给服务器,这个方式通常为httpConnection.setRequestProperty(“RANGE”,”bytes=x”);)
在我们这段代码,开启”续传“行为,即keepGoing方法中:我们起头让线程休眠10秒钟,这正是为了让我们运行程序看到效果。