Java-21 多线程

day18

1.多线程概述

  • 进程:一个程序运行,程序在内存中分配的那片空间。

  • 线程:进程中一个执行单元执行路径

    进程中至少有一个线程,如果进程中有多个线程,就是多线程的程序。

  • 并行与并发:

    并行:某一时间点,有多个程序同时执行,多核CPU运行
    并发:某一时间段,有多个程序同时执行,并不是真正意义的同时执行。 为多线程。
    
  • 并发真的是同时执行吗?

    不是,而是时间间隔很短,造成同时执行感觉。
    
  • 多线程优势?

    提高了用户体验,提高了程序的运行效率,提高CPU使用率。
    

2.开启线程两种方式

  • 开启线程

    /*
     * 1.继承Thread
     * 2.重写run方法
     * 3.创建子类的对象
     * 4.调用start方法
     * */
    public class ThreadDemo1 {
    	public static void main(String[] args) {
            // 创建子类的对象
    		Demo d1 = new Demo("Jack");
    		Demo d2 = new Demo("Tom");
            // 设置线程名字
    		d1.setName("d1");
    		// 获取d1执行线程名字
    		System.out.println(d1.getName());
    		// 获取当前线程名字
    		System.out.println(Thread.currentThread().getName());
            
            // 调用start方法
    		d1.start();
    		d2.start();
    	}
    }
    // 继承Thread
    class Demo extends Thread{
    	String nickName;
    	public Demo(String nickName) {
    		this.nickName = nickName;
    	}
        // 重写run方法
    	public void run() {
    		for(int i=0;i<30;i++) {
    			System.out.println(nickName + "---" + i);
    		}
    	}
    }
    // 整个运行过程有三个线程运行,主线程开启d1和d2线程
    
  • run方法与start方法区别

    start:开启新的线程,会自动调用run方法在新的线程中执行
    run:没有开启新的线程,只是普通方法
    
  • 开启新线程第二种方式

    声明实现Runnable接口的类,该类然后实现run方法,然后可以分配该类的实例,在创建Thread时做为一个参数来传递并启动,采用这种风格的同一个例子
    
    /*
     * 实现多线程第二种方式:
     * 1.实现Runnable
     * 2.重写run方法
     * 3.创建Runnable子类的对象
     * 4.创建Thread类的对象,把第三步的对象传到构造方法中
     * 5.使用Thread子类对象,调用start方法
     * */
    public class ThreadDemo2 {
    	public static void main(String[] args) {
    		Demo5 d = new Demo5();// 只有Thread或子类线程对象才是线程对象。它只是线程任务度夏宁。
    		Thread th = new Thread(d); // th才是线程对象
    		Thread th2 = new Thread(d);
    		th.start();
    		th2.start();
    	}
    }
    
    class Demo5 implements Runnable{
    	public void run() {
    		for (int i=0;i<20;i++) {
    			System.out.println(Thread.currentThread().getName() + "---" + i);
    		}
    	}
    }
    
  • 两种实现方式,区别是?

    第一种方式有局限性的,因为Java是单继承,如果一个类已经有一个继承,它就不能再继承Thread类,就无法实现多线程。而第二种实现通过接口方式实现更合理,并且第二种方式更加符合面向对象特点:高内聚低耦合,把线程对象和线程任务分离开了。
    

3.线程中方法

1.sleep方法使用

public Static void sleep(long millis)
静态方法 进入阻塞状态,时间结束后进入可执行
  • 示例:
Thread.sleep(3000);

sleep方法让谁阻塞,取决于他在哪个线程中。

2.join方法使用

public final void join()
被谁调用,让哪个线程先执行,执行完毕后,再执行所在线程
  • 示例

public class JoinDemo1 {
	public static void main(String[] args) {
		Sum s = new Sum();
		s.start();
		// join:用谁调用,就让那个线程先执行,执行完毕后,再执行他所在线程(将他所在线程阻塞)。
		try {
			s.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.println(Sum.sum);
	}
}

class Sum extends Thread{
	static int sum = 0;
	public void run() {
		for(int i=0;i<=1000;i++) {
			sum += i;
		}
	}
}

3.yield方法

public static void yield()
让其他线程先执行,不一定生效,因为让谁执行是CPU决定的

4.stop方法

停止一个线程

5.interrupt 方法

打断线程的阻塞状态,进入可执行状态,会抛出异常
  • 示例:打断子线程阻塞
public class interruptDemo {
	public static void main(String[] args) {
		Demo3 d = new Demo3();
		d.start();
		// 将d执行线程阻塞状态打断。
		d.interrupt();
		System.out.println("over");
	}
}


class Demo3 extends Thread{
	public void run() {
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		for (int i=0;i<10;i++) {
			System.out.println(i);
		}
	}
}
  • 打断主线程阻塞
public class InterruptDemo2 {
	public static void main(String[] args) {
		// 主线程传给Demo4
		Demo4 d = new Demo4(Thread.currentThread());
		d.start();
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}	
		System.out.println("over");
	}
}

class Demo4 extends Thread{
	Thread th;
	public Demo4(Thread th) {
		this.th = th;
	}
	// 用于打断主线程
	public void run() {
		th.interrupt();
	}
}
  • 练习:创建两个线程,一个线程负责打印大写字母表,一个线程负责打印小写字母表
public class Threadlianxi1 {
	public static void main(String[] args) {
		Thread th1 = new Thread(new Task1());
		Thread th2 = new Thread(new Task2());
		th1.start();
		th2.start();
	}
}


class Task1 implements Runnable{
	public void run() {
		for(char i='a';i<='z';i++) {
			System.out.println(i);
		}
	}
}
class Task2 implements Runnable{
	public void run() {
		for(char i='A';i<='Z';i++) {
			System.out.println(i);
		}
	}
}

4.线程生命的周期

主线程执行时候在栈空间,开辟空间给子线程,而他们的之间栈是独立的,但他们堆空间数据是共享的

5.线程安全的问题

  • 买票示例:
public class SellTicketsDemo {
	public static void main(String[] args) {
		Tickets t = new Tickets();
		Thread th1 = new Thread(t);
		Thread th2 = new Thread(t);
		Thread th3 = new Thread(t);
		th1.start();
		th2.start();
		th3.start();
	}
}


class Tickets implements Runnable{
	static int tickets = 100;
	public void run() {
		while (true) {
			if (tickets> 0) {
				try {
					Thread.sleep(30);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets--  +"张票!");
			} else {
				break;
			}
		}
	}
}

  • 在执行代码时候,会发现多个线程会卖出同一张票。这样会产生线程安全问题。

  • 线程安全产生原因:1.具备多线程。2.操作共享数据。3.操作共享数据的代码有多条。

  • 通过加锁:让每一时刻只能有一个线程操作数据。方式有三种

1.同步代码块

synchronized(锁对象){
	容易产生线程安全问题的代码
}

// 锁对象:可以是任意对象,但是必须保证多个线程使用是同一个对象。
  • 示例:
    • 关键点:锁对象选择
public class SellTicketsDemo {
	public static void main(String[] args) {
		Tickets t = new Tickets();
		Thread th1 = new Thread(t);
		Thread th2 = new Thread(t);
		Thread th3 = new Thread(t);
		th1.start();
		th2.start();
		th3.start();
	}
}


class Tickets implements Runnable{
	static int tickets = 100;
    // 如果o方法run方法里,则无法实现线程安全,原因是执行run方法,实现3个o对象,对于这三个线程来说o对象不是共有的同一个对象。
	Object o = new Object();
	public void run() {
		while (true) {
			synchronized (o) {// o所在的类被new几次
				if (tickets> 0) {
					try {
						Thread.sleep(30);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets--  +"张票!");
				} else {
					break;
				}
			}
		}
	}
}
  • 锁对象选取错误示例:
    • Tickets2被new了3次导致,没有锁住。
public class SellTicketsDemo2 {
	public static void main(String[] args) {
		Tickets2 th1 = new Tickets2();
		Tickets2 th2 = new Tickets2();
		Tickets2 th3 = new Tickets2();
		th1.start();
		th2.start();
		th3.start();
	}
}

class Tickets2 extends Thread{
	static int tickets = 100;
	Object o = new Object();
	public void run() {
		while (true) {
			synchronized (o) {
				if (tickets> 0) {
					try {
						Thread.sleep(30);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets--  +"张票!");
				} else {
					break;
				}
			}
		}
	}
}
  • 方式1:解决方式通过:构造方法解决:
public class SellTicketsDemo2 {
	public static void main(String[] args) {
		Object o = new Object();
		Tickets2 th1 = new Tickets2(o);
		Tickets2 th2 = new Tickets2(o);
		Tickets2 th3 = new Tickets2(o);
		th1.start();
		th2.start();
		th3.start();
	}
}

class Tickets2 extends Thread{
	static int tickets = 100;
	Object o;
    // 构造方法
	public Tickets2(Object o) {
		this.o=o;
	}

	public void run() {
		while (true) {
			synchronized (o) {
				if (tickets> 0) {
					try {
						Thread.sleep(30);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets--  +"张票!");
				} else {
					break;
				}
			}
		}
	}
}
  • 方式2:使用静态方式:
Static Object o = new Object();
  • 方式3:使用字符串的常量
synchronized ("abc")
  • 方式4:使用Class对象
synchronized (SellTicketsDemo2.class)
// 在jvm中,每一个类的Class总共只有一个

2.同步方法

  • 把synchronized放到方法的修饰符中,锁的是整个方法。
public class SellTicketsDemo3 {
	public static void main(String[] args) {
		Tickets3 th1 = new Tickets3();
		new Thread(th1).start();
		new Thread(th1).start();
		new Thread(th1).start();
	}
}

class Tickets3 implements Runnable{
	static int tickets = 100;
	// 默认锁对象:this
	public synchronized void run() {
		while (true) {
				if (tickets> 0) {
					try {
						Thread.sleep(30);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets--  +"张票!");
				} else {
					break;
				}
		}
	}
}

上述代码虽然解决了线程安全问题,但是编程了单线程程序,原因synchronized锁的范围太大,第一个进来线程,执行完整个while循环。导致在使用同步方法时候也需要注意这个问题。

  • 但可以通过同步方法解决懒汉式单例模式:
package com.xjk;
// 懒汉式:存在问题,存在线程安全问题
public class Singleton2 {
	private static Singleton2 s;
	// 构造方法私有化,为了不让别人随便new
	private Singleton2() {
	}
	// 通过synchronized 解决线程安全问题
	public synchronized static Singleton2 getInstance() {
		if (s == null) {
			s = new Singleton2();
		} 
		return s;
	}
}
// 当启100个线程,当一个线程执行到s=new Singleton2();此时刚要new Singleton2,cpu切到第二个线程,因为s此时还是等于null,第二个线程也执行new Singleton2() 导出单例模式创建多个对象。此时通过同步方法添加synchronized可以有效解决此问题。
  • 同样StringBuffer是线程安全的内部有synchronized,效率相对StringBuilder低,StringBuilder是线程不安全的。

3.Lock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTicketsDemo4 {
	public static void main(String[] args) {
		Tickets4 th1 = new Tickets4();
		new Thread(th1).start();
		new Thread(th1).start();
		new Thread(th1).start();
	}
}

class Tickets4 implements Runnable{
	static int tickets = 100;
	// 也要保证该锁对象对于多个线程是同一个
	Lock lock = new ReentrantLock();
	public synchronized void run() {
		while (true) {
				lock.lock();// 加锁
				try {
					if (tickets> 0) {
					try {
						Thread.sleep(30);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets--  +"张票!");
				} else {
					break;
				}
				} finally {
					lock.unlock();//解锁
				}
		}
	}
}
  • 释放锁的代码放到finally代码块中,否则容易造成程序阻塞。

6.死锁

  • 是指两个或两个以上的线程在执行的过程中,因争夺资源产生一种互相等待现象。

7.线程池用法

  • 线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  • 可以根据系统的承受能力,调整线程池中工作线程数目,放置因为消耗过多的内存,而把服务器累死(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

  • 线程池的创建:

    public static ExecutorService newCachedThreadPool()
    	创建一个具有缓存功能的线程池
    public static ExecutorService new FixedThreadPool(int nThreads)
    	创建一个可重用的,具有固定线程数的线程池
    public static ExecutorService newSingleThreadExecutor()
    	创建一个只有单线程的线程池,相当于上个方法的参数是1
    
  • 示例:

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ThreadPoolDemo {
    	public static void main(String[] args) {
            // 1
    		ExecutorService pool = Executors.newCachedThreadPool();
    		pool.execute(new Runnable() {
    			public void run() {
    				System.out.println("hello world");
    			}
    		});
    		// 关闭线程池
    		pool.shutdown();
            
            // 2
            pool = Executors.newFixedThreadPool(20);
            
    	}
    }
    // 缓存线程池,如果没有任务会等待60s就关闭
    

8.wait/Notify/NotifyAll

  • wait,notify,notifyAll这三个方法都是Object中方法,并且这三个方法必须在同步方法或同步代码块中使用。
wait: 让线程进入等待状态,进入等待状态线程会释放锁对象(也就是允许其他线程进入同步代码块),直到使用相同的锁对象调用notify/notifyAll方法才会结束阻塞状态。
notify:唤醒,会从等待池中唤醒一个处于等待状态的线程,使其进入可执行状态。
notifyAll:唤醒全部,会将等待池中所有处于等待状态的线程唤醒,使其进入可执行状态。

​ notify或notifyAll以后,被唤醒的线程并不是立马执行,需要等到notify,notifyAll所在代码块执行完毕后才会执行。因为只有同步代码块执行完毕后,才会释放锁对象,其他线程才可以进来。

​ wait方法会释放锁对象,也就是一个线程使用wait进入等待状态后,允许其他线程进入同步代码块。而sleep方法不会释放锁对象,到时间后自己会醒来。

  • 示例:
public class WaitNotifyDemo {
	public static void main(String[] args) {
		Object o = new Object();
		// 当使用
		new Thread(new Demos1(o)).start();
		new Thread(new Demos1(o)).start();
		try {
			// sleep 作用是保证上面2个线程都执行到wait,然后第三个线程可以使用notifyAll解除阻塞
			Thread.sleep(30);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		new Thread(new Demos2(o)).start();
	}
}

class Demos1 implements Runnable{
	Object o;
	public Demos1(Object o) {
		this.o = o;
	}
	@Override
	public void run() {
		synchronized(o) {
			System.out.println(Thread.currentThread().getName() + "Wait ,,,start...");
			try {
				// 阻塞线程
				o.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("等待结束...");
		}
	}
}

class Demos2 implements Runnable{
	Object o;
	public Demos2(Object o) {
		this.o = o;
	}
	@Override
	public void run() {
		synchronized(o) {
			System.out.println(Thread.currentThread().getName() + "notify ,,,start...");
			// 解除阻塞
             // o.notify();
			o.notifyAll();
			System.out.println(Thread.currentThread().getName() + "notify ,,,end...");
		}
	}
}
/*
Thread-0Wait ,,,start...
Thread-1Wait ,,,start...
Thread-2notify ,,,start...
Thread-2notify ,,,end...
等待结束...
等待结束...
 * */

​ 注意:

​ 1.wait.notify.notifyAll 一定要在同步代码块中执行,使用锁对象调用。

​ 2.要想能唤醒wait,必须使用同一个锁对象调用notify/notifyAll

​ 3.wait方法会释放锁对象,进入了等待状态以后,允许其他线程进入同步代码块执行。

​ 4.notify方法唤醒了以后,wait不是立马执行,等待notify中代码执行完毕。

​ 5.而notify方法只能唤醒一个(随机唤醒),而notifyAll全部都能唤醒

  • 为什么wait,notify,notfiyAll 放到Object类中?

    因为他们使用锁对象调用,锁对象可以是任意对象,任意对象都有的方法定义在Object
    

9.定时器

  • 方法

    public void schedule(TimerTask task, long delay) 延迟多少毫秒后执行定时任务
    public void schedule(TimerTask task, Date date) 指定时间执行定时任务
    public void schedule(TimerTask task, long delay, long period) 延迟执行,指定间隔后循环执行
    public void cancel() 取消定时任务
    
  • 示例1:

    import java.util.Timer;
    import java.util.TimerTask;
    
    public class TimerDemo {
    	public static void main(String[] args) {
    		Timer timer = new Timer();
    		// 5000毫秒执行一次任务,同一个定时任务只能执行一次
    		// 它是多线程启动的
    		timer.schedule(new TimerTask() {
    			public void run() {
    				System.out.println("你好");
    				// 取消定时任务,一般放到定时任务中
    				timer.cancel();
    			}
    		}, 5000);
    	}
    }
    
    
  • 示例2:日期定时

    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class TimerDemo {
    	public static void main(String[] args) throws ParseException {
    		Timer timer = new Timer();
            // 如果时间已经过期,它会立刻运行
    		Date d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2020-10-10 10:10:10");
    		timer.schedule(new TimerTask() {
    			public void run() {
    				System.out.println("起床了!");
    			}
    		}, d);
    	}
    }
    
  • 示例3:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class TimerDemo2 {
    	public static void main(String[] args) {
    		// 3秒后执行,每隔1秒执行一次
    		new Timer().schedule(new TimerTask() {
    			public void run() {
    				System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    			}
    		},3000,1000);
    	}
    }
    
    
原文地址:https://www.cnblogs.com/xujunkai/p/13888474.html