Java学习笔记:多线程
文章目录
1.基本概念
程序Program:为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程Process:程序的一次执行过程,或正在运行的一个程序(动态过程,有生命周期:自身产生、存在、消亡)。
线程Thread:一个程序内部的一个执行路径。
线程生命周期:
并行与并发:
并行:多个CPU同时执行多个任务。如:河师大球场上同时有多场地在进行球赛。
并发:一个CPU(采用时间片)同时执行多个任务。如:某一个场地许多人在打一个球。
线程的优先级:(优先级高优先执行的概率高,并不是优先级高就一定先执行)
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 --> 默认的优先级
如何设置当前线程的优先级:
getPriority()
setPriority(int p)
2.创建多线程的方式(四种)
方式一:继承Thread类
步骤:
1.创建一个继承于Thread类的子类
2.重写Thread类的run()
3.创建Thread类的子类对象
4.调用Thread类的start() 作用:①启动当前线程②调用当前线程的run()
class MyThread extends Thread {
//2.重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread t0 = new MyThread();
//4.调用Thread类的start() 作用:①启动当前线程②调用当前线程的run()
t0.start();
MyThread t1 = new MyThread();
t1.start();
//如下操作仍在main线程中
for (int i = 0; i < 10000; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i + "****主线程********");
}
}
}
说明:
Thread中的常用方法:
1.start() 启动当前线程;调用当前线程的run()
2.run() 通常需要重写此方法,将创建线程需要执行的操作声明在此方法中
3.currentThread() 静态方法,返回执行当前代码的线程
4.getName() 获取当前线程的名字
5.setName() 设置当前线程的名字
6.yield() 线程让步,释放当前cpu的执行权,(但是下一刻可能又被此线程抢回来)
7.join() 在线程a中调用线程b的join(),此刻线程a进入阻塞状态,需要等待线程b完全执行完后,线程a才结束阻塞状态。
8.stop() 已过时,执行此方法时,强制结束当前线程。
9.sleep(long millis) 让当前线程睡眠,参数是指millis毫秒内,当前线程是阻塞的
10.isAlive() 判断当前线程是否存活(是否执行完run()),返回true和false
class HelloThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "优先级" + Thread.currentThread().getPriority() + ":" + i);
}
if (i % 2 == 20) {
yield();
}
}
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
HelloThread h1 = new HelloThread();
h1.setName("线程一");
//设置分线程优先级
// h1.setPriority(Thread.MAX_PRIORITY);
h1.start();
Thread.currentThread().setName("主线程");
//设置主线程优先级
// Thread.currentThread().setPriority(2);
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + "优先级" + Thread.currentThread().getPriority() + ":" + i);
}
if (i == 20) {
try {
h1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(h1.isAlive());
}
}
方式二:实现Runnable接口
步骤: 1.创建一个实现Runnable接口的类
2.实现类去实现接口中的抽象方法run()
3.创建实现类对象
4.将此对象作为参数传递给Thread类构造器,创建Thread类的对象
5.通过Thread类的对象调用start()
//1.创建一个实现Runnable接口的类
class MThread implements Runnable {
//2.实现类去实现接口中的抽象方法run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3.创建实现类对象
MThread mThread = new MThread();
//4.将此对象作为参数传递给Thread类构造器,创建Thread类的对象
Thread t1 = new Thread(mThread);
//5.通过Thread类的对象调用start()
t1.start();
}
}
说明:
比较创建线程的前两种方式:
开发中优先选择实现Runnable接口的方式,原因:
1.实现的方式没有类的单继承的限制。
2.实现的方式更适合来处理多个线程有共享数据的情况。
二者的联系:public class Thread implements Runnable
相同点:两种方式都需要重新run()方法,将需要执行线程的逻辑写在run()方法中。
方式三:实现Callable接口
**步骤:**1.创建一个Callable实现类
2.实现call()方法,将此线程要实现的操作写在此处
3.创建Callable接口实现类的对象
4.将此Callable对象当做参数传递给FutureTask构造器中构造FutureTask对象
5.将FutureTask对象当做参数传递给Thread类构造器
6.调用start()开启线程
7.获取Callbale中call()方法的返回值
//1.创建一个Callable实现类
class NumThread implements Callable {
//class NumThread implements Callable<Integer>{
//2.实现call()方法,将此线程要实现的操作写在此处
@Override
public Object call() throws Exception {
// public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
sum += i;
System.out.println(i);
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable对象当做参数传递给FutureTask构造器中构造FutureTask对象
FutureTask futureTask = new FutureTask(numThread);
// FutureTask<Integer> futureTask = new FutureTask<Integer>(numThread);
//5.将FutureTask对象当做参数传递给Thread类构造器
Thread thread = new Thread(futureTask);
//6.调用start()开启线程
thread.start();
try {
//7.获取Callbale中call()方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()方法的返回值
Object o = futureTask.get();
// Integer o = futureTask.get();
System.out.println("sum为:" + o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
说明:
如何理解实现Callable接口的方式创建多线程比实现Runnable接口的方式强大?
1.call()可以有返回值。
2.call()可以抛出异常,打印一下即可捕获。
3.Callable支持泛型。
方式四:线程池
**步骤:**1.提供指定线程数量的线程池
2.执行指定的线程操作。需要提供Runnable或Callable接口的实现类对象作为参数
3.关闭线程池
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池属性
//ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
//service1.setMaximumPoolSize(15);
//2.执行指定的线程操作。需要提供Runnable或Callable接口的实现类对象作为参数
NumberThread a1 = new NumberThread();
service.execute(a1);//适用于Runnable接口
//service.submit(Callable callable)//适用于Callbale接口
//3.关闭线程池
service.shutdown();
}
}
说明:
好处:1.提高响应速度(减少创建新线程的时间)
2.降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
3.便于线程管理:
corePoolSize:核心池的大小
MaximumPoolSize:最大线程数
KeepAliveTime:线程没有任务时最多多长时间后终止
3.线程安全问题
**引入:**创建三个窗口买票,总共100张,用实现Runnable接口的方式
class Window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName()+"票号:"+ticket);
ticket--;
}else break;
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
此例子存在线程的安全问题,
1.问题:卖票过程中出现了重票、错票----> 出现线程的安全问题
2.问题出现的原因:在某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
3.如何解决:当一个线程a在操作共享数据(ticket)的时候,其他线程不能参与进来(即使a阻塞也不行),直到线程a操作完ticket,其他线程才可以开始操作ticket。
4.在Java中,我们通过同步机制,来解决线程的安全问题。
5.同步的方式,解决了线程的安全问题–好处
操作同步代码块时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低–局限性
解决线程安全问题的方式一:同步代码块
synchronized(同步监视器){
* //需要被同步的代码
* }
说明: 1.操作共享数据的代码,即为需要被同步的代码
2.共享数据:多个线程共同操作的数据
3.同步监视器:俗称锁。任何一个类的对象都可以充当锁
要求:多个线程必须共用同一把锁。
补充:实现Runnable接口创建多线程的方式中,可以考虑使用this当同步监视器。在使用同步代码块解决继承Thread类的方式解决线程安全问题,慎用this当同步监视器。
使用同步代码块解决继承Thread类的方式解决线程安全问题:
class Window2 extends Thread {
private static int ticket = 100;
static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (Window2.class) { //Class clazz = Window.class,Window.class只会加载一次
//synchronized (obj)是正确的
//synchronized (this)是错误的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 window1 = new Window2();
Window2 window2 = new Window2();
Window2 window3 = new Window2();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
使用同步代码块解决实现Runnable接口的方式解决线程安全问题:
class Window1 implements Runnable {
private int ticket = 100;
// Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (this) { //此时this:唯一的Window1的对象 //synchronized (obj){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号:" + ticket);
ticket--;
} else break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
解决线程安全问题的方式二:同步方法
如果操作共享数据的代码完整声明在一个方法中,我们不妨将此方法声明成同步的。
1.同步方法依然涉及同步监视器,只是不需要我们显示声明。
2.非静态的同步方法,同步监视器是 this(当前类创建的一个对象w1)
静态的同步方法,同步监视器是 当前类本身
使用同步方法解决继承Thread类的方式解决线程安全问题:
class Window4 extends Thread{
private static int ticket = 100;
static Object obj = new Object();
@Override
public void run() {
while (true){
show();
}
}
private static synchronized void show(){ //同步监视器为Window4.class
//private synchronized void show(){ 同步监视器window1,window2,window3,此种方法是错误的
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"票号:"+ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 window1 = new Window4();
Window4 window2 = new Window4();
Window4 window3 = new Window4();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
用同步方法解决实现Runnable接口创建多线程的方式中的安全问题:
class Window3 implements Runnable {
private int ticket = 100;
// Object obj = new Object();
@Override
public void run() {
while (true) {
//此时this:唯一的Window1的对象 //synchronized (obj){
show();
}
}
private synchronized void show() { // 此时同步监视器是this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号:" + ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w1 = new Window3();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
解决线程安全问题的方式三---->Lock锁 JDK5.0新增
步骤:1.实例化ReentrantLock 2.调用锁定方法:lock() 3.调用解锁方法:unLock()
建议优先使用顺序:Lock–>同步代码块–>同步方法
==面试题:==synchronized与Lock异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码块后,自动释放同步监视器; Lock需要手动的启动同步(lock()),同时结束同步也需要手动实现(unlock())
用Lock锁方式解决线程安全问题:
class Window implements Runnable {
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法:lock()
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票 票号:" + ticket);
ticket--;
} else break;
} finally {
//3.调用解锁方法:unLock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
线程安全问题小练习:使用同步机制将单例模式中的懒汉式改写为线程安全的
public class Bank {
private Bank() {
}
private static Bank instance = null;
public static Bank getInstance() {
//方式一:效率稍差
// synchronized (Bank.class) {
// if (instance == null) {
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
4.线程通信
引入:演示线程死锁
1.死锁的理解:不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成死锁。
2.说明:
1)出现死锁后,不会出现异常,不会出现提示,所有线程处于阻塞状态无法继续。
2)使用同步时,尽量避免死锁。
代码:
public class DeadLockTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append(1);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append(2);
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append(3);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append(4);
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
解决方法:线程通信
线程通信的例子:打印100个数,两个线程交替打印
涉及三个方法:
1.wait():一旦执行此方法,线程进入堵塞状态,并释放同步监视器。
2.notify():一旦执行此方法,就会唤醒一个被wait的线程,如果有多个线程被wait则唤醒优先级高的。
3.notifyAll():一旦执行此方法,唤醒所有被wait的线程。
说明:
1.wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中,lock不适用。
2.wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则会出现IllegalMonitorStateException异常。
3.wait()、notify()、notifyAll()三个方法是定义在java.lang.Object类中。
class Number implements Runnable {
private int i = 1;
Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (this) {
//省略this,this.notify(),this指当前Number类
notify();
if (i < 100) {
System.out.println(Thread.currentThread().getName() + ":" + i);
i++;
} else break;
try {
//省略this,this.wait(),this指当前Number类
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CommunicateTset {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
面试题: sleep()与wait()异同?
1)相同点:执行时都会使当前线程进入阻塞状态。
2)不同点:
1.两方法声明位置不同:Thread类中声明sleep(),Object类中声明wait()。
2.调用的要求不同:sleep()可以再任意地方调用,wait()只能以同步代码块或同步方法中的同步监视器调用。
3.关于释放同步监视器:sleep()调用时不会释放同步监视器,wait()会释放同步监视器。
注:这是我跟着B站视频尚硅谷免费公开视频学习java时自己记得笔记,头一次发有许多不足或者不合适的地方,希望多多指正,多多包涵^^