-
线程的生命周期
-
线程安全问题的举例和解决措施
-
同步代码块处理实现Runnable的线程安全问题
-
同步代码块处理继承Thread类的线程安全问题
-
同步方法处理实现Runnable的线程安全问题
-
同步方法处理继承Thread类的线程安全问题
-
线程安全的懒汉式单例模式
-
死锁问题
-
Lock锁方式解决线程安全问题
-
同步机制练习
-
线程通信的例题
-
sleep()和wait()的异同
-
线程通信的生产者消费者例题
-
创建多线程的方式三:实现Callable接口
-
使用线程池的好处
-
创建多线程的方式四:使用线程池
1,线程的生命周期
Java用Thread类的内部类State(枚举类)定义了线程的几种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 当调用start()创建一个线程后,该线程不会立即被执行,而是进入就绪状态,此时可能有其他之后创建的线程也进入了就绪状态,CPU选择一个线程执行,这个线程可能是先创建的,也可能是后创建的;运行时的线程可能会因为CPU调度而失去执行权,变为就绪状态,等待CPU下一次调度而再被执行;而如sleep()等操作会让正在执行的线程进入阻塞状态,此时CPU的执行权不可能轮到它,CPU只会选择就绪状态的线程,所以这是一种不同于就绪的状态,当线程结束阻塞就会回到就绪状态,此时它可以被CPU调度执行;所有线程的结局只有一个,就是执行完或者发生其他情况变为死亡的状态
2,线程安全问题的举例和解决措施
以取钱为例:在每次取钱的操作之前,都要先判断余额是否大于要取的钱。如果两个线程同时对一个余额进行取钱操作,当第一个线程执行时先判断,发现3000<2000,恰好此时CPU发生调度,该线程进入就绪状态,第二个线程被执行,发现3000<2000,然后取走了2000,轮到第一个线程接着上次的状态继续执行,也取走了2000,此时余额变为-1000。这种情况显然是不允许的 两个线程对两份不同的数据进行操作不会出现线程安全问题,只有对同一数据操作时可能会出现。在之前的卖票例子中、单例模式的懒汉式实现中都可能出现线程安全问题
3,同步代码块处理实现Runnable的线程安全问题
线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作package com.atguigu.java;
/**
* 卖票过程中出现重票、错票的线程安全问题
* 原因:某个线程在操作车票的过程中,操作尚未完成就切换到其他线程执行
* 解决:当线程a操作ticket时,其他线程不能参与进来,直到a操作完,其他线程才可以操作ticket,
* 这种情况即使a线程出现了阻塞也不能改变
* Java中通过同步机制解决线程安全问题
* 方式一:同步代码块
* synchronized(同步监视器) { // 同步监视器即锁,任何一个类的对象都可以充当锁,要求多个线程必须共用同一把锁
* // 需要被同步的代码 (操作共享数据的代码,共享数据是多个线程共同操作的变量,比如ticket)
* }
*
* 方式二:同步方法
*
* 同步的方式可以解决线程安全问题。但在执行同步代码时,只能有一个线程参与,其他线程必须等待,这相当于是
* 单线程执行,效率变低
*/
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
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();
}
}
class Window1 implements Runnable {
private int ticket = 100;
Object obj = new Object(); // 只有一个Window1对象,也就只有一个Object对象
@Override
public void run() {
// Object obj = new Object(); // 此时每个线程执行run()时都会创建一个Object对象,也就不是共用同一把锁了,所以会出现线程安全问题
while(true) {
synchronized(obj) { // 不加这一行时,就会出现下面的重票、错票情况
// synchronized(this) // this表示唯一的那个Window1对象,所以这样写也是可以的
if (ticket > 0) { // 当某个线程刚判断完就轮到另一个线程执行,就可能出现错票,比如出现为0或-1的票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); // 当某个线程刚打印完,就轮到另一个线程执行,就可能出现重票
ticket--;
} else {
break;
}
}
}
}
}
4,同步代码块处理继承Thread类的线程安全问题
package com.atguigu.java;
/**
* synchronized包含的代码既不能多也不能少,如果还有操作共享数据的代码没有包含,则可能
* 出现线程安全问题,如果包含多了,比如这个例子中将while循环包含进去,则会出现一个
* 线程执行完所有的操作,其他线程根本得不到执行机会
*/
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window2 extends Thread {
private static int ticket = 100;
private static Object obj = new Object(); // 创建了三个Window2对象,所以要将obj定义为static以保证只有一个对象
@Override
public void run() {
while(true) {
synchronized(Window2.class) { // 正确的,说明类也是对象,且这个类只加载一次,只有一个对象。Class clazz = Window2.class,将一个类赋值给类类型的引用变量
// synchronized(this) // 错误的,此时不同的Window2对象调用run(),this代表不同的对象
// synchronized(obj) //正确的,Object对象只有一个
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
5,同步方法处理实现Runnable的线程安全问题
package com.atguigu.java;
/**
* Java同步机制解决线程安全问题的方式之二:同步方法
* 将操作共享数据的代码放在一个方法中,并将此方法声明为同步的
*/
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
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();
}
}
class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(true) {
show(); // 调用同步方法,也能解决线程安全问题
}
}
private synchronized void show() { // 同步方法,也有同步监视器,要求是唯一的,默认为this
// synchronized(this) { // 同步代码块的方式
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
// }
}
}
6,同步方法处理继承Thread类的线程安全问题
package com.atguigu.java;
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while(true) {
show();
}
}
private static synchronized void show() { // 同步监视器:Window4.class即当前类本身
// private synchronized void show() {} // 错误的,此时同步监视器是this,本例中就是三个Window4对象
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); // 在静态方法中不能调用非静态结构
ticket--;
}
}
}
7,线程安全的懒汉式单例模式
package com.atguigu.java1;
/**
* 采用同步方法或同步代码块的方式
*/
public class BankTest {
}
// 懒汉式单例模式
class Bank {
private Bank() {}
private static Bank instance = null;
// public static synchronized Bank getInstance() // 同步方法,同步监视器为Bank.class
public static Bank getInstance() { // 如果两个线程都通过run()调用getInstance(),第一个线程在判断instance为null时
// 发生调度,第二个线程调用该方法并创建了对象,回到第一个线程它又会创建一个对象
// 方式一:效率较差,当创建了单例对象,之后的线程调用该方法都要先判断instance是否为null
// 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;
}
}
8,死锁问题
package com.atguigu.java1;
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() { // 通过Thread类的匿名子类的匿名对象调用start()开启一个线程执行重写的run()
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100); // 增加发生死锁的概率,当开启的第一条线程执行并拿到s1锁后进入阻塞,
// 此时开启的第二天线程执行并拿到s2锁,就形成了接下来二者都想
// 拿到对方拥有的锁的局面,就形成了死锁,此时程序无法结束
} 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() { // 通过向Thread类传递一个实现Runnable接口的匿名实现类的匿名对象创建
// Thread类的匿名对象,调用start()开启一个线程并执行重写后的run()
@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();
}
}
另一个例子,有时候可能发生死锁的地方比较隐蔽,程序执行没问题不一定代表不可能发生死锁,应尽量避免死锁的产生
package com.atguigu.java1;
//死锁的演示
class A {
public synchronized void foo(B b) { //同步监视器:A类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {//同步监视器:A类的对象:a
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {//同步监视器:b
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
9,Lock锁方式解决线程安全问题
package com.atguigu.java1;
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线程安全的方式三:Lock锁
* 1,实例化ReentrantLock
* 2,将可能出现线程安全的代码放在try{}中,并调用ReentrantLock对象的lock方法
* 3,在finally中调用ReentrantLock对象的unlock方法
*
* 面试题:synchronized和Lock的异同
* 相同点:都可以解决线程安全问题
* 不同点:synchronized机制(同步代码块或同步方法)在执行完相应的代码后,自动释放同步监视器,
* Lock则需要手动启动同步、手动结束同步
* 面试题:解决线程安全问题的方式有哪些
* synchronized机制(同步方法和同步代码块)、Lock锁
*/
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();
}
}
class Window implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock(); // 如果是继承Thread的方式,就要加上static,避免每个对象都有一把锁
@Override
public void run() {
while(true) {
try {
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 { // 无论什么情况,都会执行
lock.unlock(); // 释放锁
}
}
}
}
10,同步机制练习
package com.atguigu.exer;
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
public synchronized void deposit(double amt) { // 同步监视器使用唯一的Account对象
// public void deposit(double amt) // 会出现线程安全问题
if(amt > 0) {
balance += amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "存钱成功,余额为:" + balance);
}
}
}
class Customer extends Thread {
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for(int i = 0 ; i < 3 ; i++) {
acct.deposit(1000);
}
}
}
11,线程通信的例题
package com.atguigu.java2;
/**
* 两个线程交替打印1-100
* 这个过程是:
* 一开始线程一拿到锁(同步监视器),执行notify()没有影响,调用sleep()使自己进入阻塞状态但并不会切换
* 线程执行,因为锁还是线程一的,执行完对共享数据的操作后,wait()使线程一进入阻塞,并且释放了锁
* 接下来一定是线程二被执行,它先拿到锁,执行notify()唤醒了线程一,因为线程二拿着锁,所以不会切换到
* 线程一,同样线程二执行sleep()进入阻塞,然后执行对共享数据的操作,接着执行wait()使自己进入
* 阻塞并释放了锁。二者如此交替执行
* 三个和线程通信相关的方法
* wait():执行此方法,该线程就会进入阻塞状态,并释放同步监视器
* notify():执行此方法,该线程就会唤醒一个wait()过的阻塞线程;如果有多个线程执行过wait(),则唤醒优先级最高的那个
* notifyALL():执行此方法,该线程唤醒所有wait()过的阻塞线程
* 注意点:
* 1,三个方法必须放在同步代码块或同步方法中(不能放在Lock中)
* 2,三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则出现异常,这也是为什么三个方法不能放在
* Lock中的原因,因为Lock中没有同步监视器
* 3,三个方法是定义在Object类中的
*/
class Number implements Runnable {
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true) {
synchronized (this) {
// synchronized(obj)
notify(); // 唤醒另一个阻塞的线程,notifyAll()唤醒所有阻塞的线程。相当于this.notify(),
// 同步监视器即为调用者,所以没报异常;如果同步监视器变为obj则会报异常
// obj.notify(); // 正确写法
if (number <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try { // 使得调用wait()的线程进入阻塞状态,不同于sleep(),调用wait()的线程会释放
// 锁(同步监视器)
wait(); // this.wait(); // 同上notify()
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
12,sleep()和wait()的异同
面试题: 相同点:调用这两个方法都会使当前线程进入阻塞状态 不同点:1,声明位置不同,sleep()声明在Thread类中,wait()声明在Object类中 2,调用的要求不同,sleep()可以在任何场景下调用,wait()必须在同步代码块或同步方法中调用 3,是否会释放同步监视器,如果都在同步代码块或同步方法中被调用,sleep()不会释放锁,wait()会释放锁13,线程通信的生产者消费者例题
package com.atguigu.java2;
/**
* 是多线程问题:生产者线程和消费者线程
* 有共享数据:店员或产品
* 涉及到线程通信的问题
*/
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
class Clerk {
private int productCount = 0;
public synchronized void produceProduct() { // 生产产品的方法。唯一的Clerk对象作为同步监视器
if(productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify(); // 从生产第一个产品开始就唤醒消费的线程。如果不加notify()就会出现这样的情况,当两个线程
// 都开启后,消费者线程先判断没有可消费的,就wait()进入阻塞,接着生产者执行,生产到20时
// 执行wait()进入阻塞
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumeProduct() { // 消费产品的方法
if(productCount > 0) {
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread { // 生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品...");
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread { // 消费者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品...");
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
14,创建多线程的方式三:实现Callable接口
package com.atguigu.java2;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* JDK5.0新增的方式
* 1,创建一个实现Callable接口的实现类,并实现call(),将此线程要执行的操作写在call()中
* 2,创建Callable接口实现类的对象,并将此对象作为参数传递给FutureTask类的构造器以创建FutureTask类的对象
* 3,将上面FutureTask类的对象作为参数传递给Thread类的构造器,并创建Thread类的对象,然后调用start()开启一个线程
* 4,如果需要call()的返回值,通过FutureTask对象的get()就可以拿到
* 相较于Runnable方式创建线程,Callable方式更强大的点
* 1,call()可以有返回值
* 2,call()可以抛出异常并被外面捕获
* 3,Callable支持泛型
*/
public class ThreadNew {
public static void main(String[] args) {
NumThread numThread = new NumThread();
FutureTask futureTask = new FutureTask(numThread);
new Thread(futureTask).start();
try {
Object sum = futureTask.get(); // get()可以拿到call()的返回值
System.out.println("总和为:" + sum);
}catch (InterruptedException e) {
e.printStackTrace();
}catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class NumThread implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1 ; i <= 100 ; i++) {
if(i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum; // 自动装箱,Integer类型
}
}
15,使用线程池的好处
16,创建多线程的方式四:使用线程池
package com.atguigu.java2;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 开发中一般都是使用线程池创建线程
* 1,创建指定线程数量的线程池
* 2,要创建一个线程并执行指定操作,需要提供实现Runnable接口或Callable接口实现类的对象
* 3,关闭线程池
* 面试题:创建多线程有几种方式
*/
public class ThreadPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10); // 创建一个大小为10的线程池,
// 该线程池是一个ExecutorService接口实现类的对象
System.out.println(service.getClass()); // 查看service的实现类,ThreadPoolExecutor
// ThreadPoolExecutor service1 = (ThreadPoolExecutor)service; // 使用下面两个方法需要强转为实现类类型,因为接口中没有这些方法
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
service.execute(new NumberThread()); // 开启一个线程
service.execute(new NumberThread1()); // 只能传入一个实现了Runnable接口的类的对象,该类实现的是Run(),线程执行的也是run()
// service.submit(); // 可以传入一个实现了Callable接口的类的对象,该类实现的是Call(),可以有返回值,submit()也返回该值
service.shutdown(); // 用完后关闭线程池
}
}
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);
}
}
}
}
class NumberThread1 implements Runnable {
@Override
public void run() {
for(int i = 0 ; i < 100 ; i++) {
if(i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}