华杉研发九学习日记24
java多线程
一,线程同步
1.1 线程同步问题
多个线程同时操作一个全局数据或静态数据时可能会造成数据冲突
解决:
- synchronized同步代码块
- synchronized同步方法
- 使用锁对象加锁解锁
// 火车站卖车票 -- 100张
public class Train implements Runnable{
int count = 100; //一百张火车票,全局变量
private boolean flag = true;
@Override
public void run() {
while(flag){ // 永久开启窗口售卖火车票
sale();
}
}
public void sale(){
// 设置一个停止售卖的条件
if(count<=0){
flag = false;
return;
}
System.out.println(Thread.currentThread().getName()+ "窗口售出了第"+count+"张火车票!");
count--;
}
}
public class TestTrain {
public static void main(String[] args) {
// 创建火车票对象
Train train = new Train();
// 启动线程
new Thread(train,"窗口一").start();
new Thread(train,"窗口二").start();
new Thread(train,"窗口三").start();
}
}
// 测试结果卖出票会出现重复和顺序混乱等问题
1.2 同步代码块
Java中采用锁来保护共享资源,防止数据不一致的情况产生。下面我们就来介绍Java中的同步机制以及synchronized关键字。
在Java中,每个对象都拥有一个"互斥锁标记",这就好比是我们说的挂锁。这个锁标记,可以用来分给不同的线程。之所以说这个锁标记是"互斥的",因为这个锁标记同时只能分配给一个线程。
光有锁标记还不行,还要利用synchronized 关键字进行加锁的操作。
synchronized 关键字有两种用法
第一种: synchronized+代码块。
synchronized(obj){
代码块;
}
synchronized关键字后面跟一个圆括号,括号中的是某一个引用,这个引用应当指向某一个对象。后面紧跟一个代码块,这个代码块被称为同步代码块""。
这种语法的含义是,如果某一个线程想要执行代码块中的代码,必须要先获得obj所指向对象的互斥锁标记。也就是说,如果有一个线程t1要想进入同步代码块,必须要获得obj对象的锁标记;而如果t1线程正在同步代码块中运行,这意味着t1有着obj对象的互斥锁标记;而这个时候如果有一个t2线程想要访问同步代码块,会因为拿不到obj 对象的锁标记而无法继续运行下去。
public void sale(){
synchronized (object){// 设置一个停止售卖的条件
if (count <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "窗口售出了第" + count + "张火车票!");
count--;
}
}
1.3 同步方法
public synchronized void sale(){
if (count <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "窗口售出了第" + count + "张火车票!");
count--;
}
1.4 使用锁对象
private Lock lock = new ReentrantLock();// 创建锁对象
// 使用锁对象上锁
public void sale(){
lock.lock(); // 上锁
if (count <= 0) {
flag = false;
lock.unlock(); // 解锁
return;
}
System.out.println(Thread.currentThread().getName() + "窗口售出了第" + count + "张火车票!");
count--;
lock.unlock(); // 解锁
}
二,死锁
如果同步代码块包裹多层时,可能会产生新的问题
(a等待b释放,b等待a释放)
synchronized(a){
synchronized(b){
代码块;
}
}
synchronized(b){
synchronized(a){
代码块;
}
}
解决死锁:
在Java中。采用了wait和notify这两个方法,来解决死锁机制。
首先,在Java中,每一个对象都有两个方法: wait和notify方法。这两个方法是定义在Object类中的方法。对某个对象调用wait(方法,表明让线程暂时释放该对象的锁标记。
synchronized(a){
a.wait();
synchronized(b){
代码块;
}
}
synchronized(b){
synchronized(a){
代码块;
a.notify();
}
}
由于可能有多个线程先后调用a对象 wait方法,因此在a对象等待状态中的线程可能有多个。而调用a.notify()方法,会从a对象等待状态中的多个线程里挑选一个线程进行唤醒。与之对应的,有一个notifyAll)方法,调用a.notifyAll会把a对象等待状态中的所有线程都唤醒。
wait作用是让当前线程阻塞,阻塞多久,取决于有没有其他线程唤醒它。notify作用是唤醒处于wait状态的线程。必须是同一个监视器下的线程。
notifyAll作用是唤醒所有处于wait状态的线程。必须是同一个监视器下的线程。
三,线程通信
不同线程之间可以相互的发信号。这就是线程通信。之所以需要进行线程通信,是因为有些时候,一个线程的执行需要依赖另外一个线程的执行结果。在结果到来之前,让线程等待(wait),有了结果只之后再进行后续的操作。对于另外一个线程而言,计算完结果,通知(notify)一下处于等待状态的线程.
线程通信借助的是Object类的wait,notify,notifyAll方法。
wait作用是让当前线程阻塞,阻塞多久,取决于有没有其他线程唤醒它。
notify作用是唤醒处于wait状态的线程。必须是同一个监视器下的线程。
notifyAll作用是唤醒所有处于wait状态的线程。必须是同一个监视器下的线程。
一般情况下,多线程里会出现线程同步的问题,我们不但要进行线程通信,还要解决线程同步的问题。
wait 与notify应用:生产者-消费者模式
这是一个比较经典的多线程场景。有商品的时候,消费者才可以消费,没有商品的时候,消费者等待。商品库存充足的时候,生产者等待,库存不满的时候,生产者生产商品。
四,线程池
什么是线程池?
水池:存放水的池子。线程池:存放线程的池子。
Java中的线程池:是一个管理线程的池子。可以在需要的时候开辟线程,可以控制最大开辟的线程个数,可以在不需要的时候关闭线程,可以让任务排队执行。这些管理过程不需要我们干预,线程池能帮我们完成。我们所要做的就是往线程池中放任务。
为什么要有线程池?
多线程解决了任务并发问题,但是开辟和关闭线程很消耗系统的性能,开辟和关闭一个线程要处理很多细节,频繁的开辟和关闭线程会给系统增加很多开销。
线程池使用了重用的概念,可以控制线程开辟的数量,复用这些线程执行任务。这样就不用频繁的开辟和关闭线程了。
线程池使用场景及优势
线程池适合处理的任务:执行时间短、工作内容较为单一。
合理使用线程池带来的好处:
1)降低资源消耗:重复利用已创建的线程降低线程创建和销毁造成的开销
2)提高响应速度:当任务到达时。任务可以不用等待线程创建就能立即执行
3)提高线程的可管理性:可以统一对线程进行分配、调优和监控
4)提供更多强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池SchedulerThreadPoolExecutor,允许任务延期执行或定期执行
线程池的作用包括:
1):利用线程池管理并复用线程、控制最大并发数等
2):实现任务线程队列缓存策略和拒绝机制
3):实现某些与时间相关的功能。如定时执行、周期执行
4)∶隔离线程环境。通过配置两个或多个线程池,将一台服务器上较慢的服务和其他服务隔离开,避免各服务线程相互影响。
三种常见的线程池
- 固定线程个数的线程池
- 不限线程个数的线程池
- 单个线程的线程池(串行任务池)
public static void main(String[] args) {
// 创建线程池
ExecutorService es = Executors.newFixedThreadPool(2);
// 提交任务 -- 把任务提交给线程池,让其从线程池中获取一个线程,并执行其当前任务
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"你好");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"哟西");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"你好");
}
});
// 关闭资源
es.shutdown();
}
拷贝:
public class TestCopy {
public static void main(String[] args) {
// 1.创建线程池
ExecutorService es = Executors.newFixedThreadPool(3);
// 2.找到其文件夹遍历其中的文件。如果是文件,则把拷贝的任务提交给战程法
File file = new File("C:\\Users\\86155\\Videos\\Counter-strike 2");
File[] files = file.listFiles();
for (File f : files) {
if (f.isFile()) {
// 把拷贝的任务交给线程池
es.submit(new Runnable() {
@Override
public void run() {
try(BufferedInputStream fis = new BufferedInputStream(new FileInputStream(f));
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream("C:\\Users\\86155\\Desktop\\张晓岚\\aa\\"+f.getName()))
) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
es.shutdown();
}
}
五,Callable接口
创建步骤;
1.创建一个子类,实现Callable接口
2.重写其中的call方法
3.创建EgtrueTask对象,封装Callable对象
4.创建Thread对象,封装FutureTask对象
5.使用start来启动线程
public class MyCall implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("------"+i);
}
return 100;
}
}
public class TestMyCall {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 实现Callable接口方式
MyCall myCall = new MyCall();
FutureTask<Integer> futureTask = new FutureTask<Integer>(myCall);
new Thread(futureTask).start();
System.out.println(futureTask.get());// 打印线程执行后返回的结果数据
// 主线程
for (int i = 0; i < 100; i++) {
System.out.println("====="+i);
}
}
}