JAVA高级笔记
一、多线程
程序、进程、线程
- 了解
- 优点
创建多线程方式一:继承Thread类
- 创建一个继承与Thread类的子类
- 重写Thread类的run() 方法 --> 将此线程执行的操作声明在run() 中
- 创建Thread类的子类的对象
- 通过此对象去调用Thread的start() 方法
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 不能使用myThread.run()来调用,这样就是单线程了
myThread.start();
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i + "abc");
}
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
- 要想启动线程,调用start() 方法,不能用run() 方法来调用
- 同一个对象不能调用两次start() 方法,想用同一套线程方案,需要再重新创建一个对象
线程的常用方法
-
start() :启动当前线程,调用当前线程的run()
-
run() :通常需要重写Thread类中的此方法,将创建的线程要执行的操作
-
currentThread() :静态方法,返回执行当前代码的线程
-
getName() :获取当前线程的名字
sout(Thread.currentThread().getName());
-
setName() :设置当前线程的名字
对象名.setName("名称"); // 给主线程命名 Thread.currentThread().setName("主线程");
还可以用构造器的方式给主线程命名
-
yield() :释放当前cpu的执行权,有可能在下一刻又会被该线程执行
-
join() :在线程a中,调用线程b的join() 方法,此时线程a就进入阻塞状态,知道线程b完全执行完之后,线程a才结束阻塞状态
例如在线程a中,调用线程b.join();(本身会抛出异常,需要用try-catch),执行join方法时a线程将就会进入阻塞状态,等b线程执行完之后才继续执行a线程
-
sleep(long millitime) :让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线城市阻塞状态
-
isAlive() :判断当前线程是否存活
线程优先级
MAX_PRIORITY: 10 --> 较高优先级
MIN_PRIORITY: 1 --> 较低优先级
NORM_PRIORITY: 5 --> 默认优先级
- getPriority() : 获取线程的优先级
- setPriority(int priority) : 设置线程的优先级
高优先级的线程要抢占低优先级cpu的执行权。但是只是从概率上讲的。
高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完之后,低优先级的线程才执行。
卖票练习
/**
* @ClassName ThreadTest2
* @Description 卖票练习测试
* @Author LiangHui
* @Date 2020/7/16 8:42
* @Version V1.0
*/
public class ThreadTest2 {
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
// 线程命名
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
// 此处还存在线程安全问题,或同时有多个线程进入if中
System.out.println(getName() + " 卖票,票号为:" + ticket);
ticket--;
}else {
break;
}
}
}
}
创建多线程方式二:实现Runnable接口
- 创建一个实现Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
public class ThreadTest3 {
/**
* @Author LiangHui
* @Description main方法,主程序入口
* @Date 2020/7/16 10:13
* @param args
* @return void
*/
public static void main(String[] args) {
// 创建实现类的对象
MyThread3 myThread3 = new MyThread3();
// 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(myThread3);
// 通过Thread类的对象调用start()
// 要看源码才知道为什么会执行MyThread类中的run()方法
// 因为target不为null 所以调用了Runnable类型的target的run()方法
t1.start();
}
}
class MyThread3 implements Runnable{
/**
* @Author LiangHui
* @Description 实现run()抽象方法
* @Date 2020/7/16 10:14
* @param
* @return void
*/
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
// currentThread() 获取当前线程
System.out.println(Thread.currentThread().getName() + "偶数" + i);
}
}
}
}
卖票练习
public class ThreadTest4 {
public static void main(String[] args) {
Window2 window2 = new Window2();
Thread t1 = new Thread(window2);
Thread t2 = new Thread(window2);
Thread t3 = new Thread(window2);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window2 implements Runnable {
// 因为只创建了一个Windows2对象,所以不用添加static
private int ticket = 100;
/**
* @Author LiangHui
* @Description 实现run()抽象方法,实现卖票功能
* @Date 2020/7/16 10:40
* @param
* @return void
*/
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 票号:" + ticket);
ticket--;
} else {
break;
}
}
}
}
比较两种创建线程的方式
-
开发中,优先选择实现先Runnable接口的方式
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况
-
联系
-
Thread类也是实现了Runnable接口的
public class Thread implements Runnable
-
两种方法都需要重写run() 方法,将线程要执行的逻辑声明在run() 中
-
线程的生命周期
线程安全问题
- 问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
- 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
- 如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
在Java中,我们通过同步机制,来解决线程的安全问题
方式一:同步代码块
synchronized(同步监视器){
// 需要被同步的代码
}
- 说明:
- 操作共享数据的代码,即为需要被同步的代码。 --> 不能包含代码多了,也不能包含代码少了。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
- 要求:多个线程必须要共用同一把锁。即同步监视器必须是相同的对象
- 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
同步代码块处理实现Runnable
public class ThreadTest5 {
/**
* @Author LiangHui
* @Description main方法,主程序入口
* @Date 2020/7/16 18:05
* @param args
* @return void
*/
public static void main(String[] args) {
Window3 window3 = new Window3();
Thread t1 = new Thread(window3);
Thread t2 = new Thread(window3);
Thread t3 = new Thread(window3);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window3 implements Runnable{
private int ticket = 100;
/**
* @Author LiangHui
* @Description 实现run()方法,实现卖票功能
* @Date 2020/7/16 17:30
* @param
* @return void
*/
@Override
public void run() {
// 在操作共享变量的地方添加同步代码块
// synchronized(同步监视器),同步监视器可以使任意的对象,前提是必须是公用的对象(锁)
while (true) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 票号:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
同步代码块处理继承Thread类
class Window4 extends Thread {
private static int ticket = 100;
/**
* @Author LiangHui
* @Description 重写run()方法,实现卖票功能
* @Date 2020/7/17 7:15
* @param
* @return void
*/
@Override
public void run() {
// 类也是对象,所以Window4.class是对象,并且是唯一的
while (true) {
synchronized (Window4.class) {
if (ticket > 0) {
System.out.println(getName() + " 票号:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
同步方法块处理实现Runnable
class Window5 implements Runnable {
private int ticket = 100;
/**
* @Author LiangHui
* @Description 实现run()抽象方法,实现卖票功能
* @Date 2020/7/16 10:40
* @param
* @return void
*/
@Override
public void run() {
// 同步方法,不能在run()方法中添加同步监视器
while (true) {
buyTicket();
}
}
/**
* @Author LiangHui
* @Description 创建同步方法,将操作共享变量的代码包含进去
* @Date 2020/7/17 8:57
* @param
* @return void
*/
private synchronized void buyTicket(){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 票号:" + ticket);
ticket--;
}
}
}
同步代码块处理继承Thread类
class Window6 extends Thread{
private static int ticket = 100;
/**
* @Author LiangHui
* @Description 重写run()方法,实现卖票功能
* @Date 2020/7/17 9:04
* @param
* @return void
*/
@Override
public void run() {
while (true) {
buyTicket();
}
}
/**
* @Author LiangHui
* @Description 创建静态的同步方法,将操作共享变量的代码包含进去
* @Date 2020/7/17 9:07
* @param
* @return void
*/
private static synchronized void buyTicket() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 票号:" + ticket);
ticket--;
}
}
}
使用同步方式的优劣
-
同步的方式,解决了线程的安全问题。—好处
-
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 —局限性
解决单例模式的懒汉式实现中的线程安全问题
public class SingletonLazyTest {
public static void main(String[] args) {
Bank bank = Bank.getInstance();
}
}
class Bank {
private Bank() {
}
// 私有化构造器
// 内部创建类的对象,并且为空
private static Bank instance = null;
// 同步方法,锁(同步监视器)-->Bank.class
// public static synchronized Bank getInstance(){
public static Bank getInstance(){
// // 存在线程安全问题
// if (instance == null) {
// instance = new Bank();
// }
// return instance;
// 同步代码块
// 方式一:效率稍差,每次new对象的时候,都要排队拿锁,等候时间长,效率稍差
// 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;
}
}
完整版
public class SingletonLazyTest {
public static void main(String[] args) {
Bank bank = Bank.getInstance();
}
}
class Bank {
private Bank() {
}
private static Bank instance = null;
public static Bank getInstance(){
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
线程死锁问题
演示死锁问题
public class ThreadTest9 {
public static void main(String[] args) {
StringBuffer str1 = new StringBuffer();
StringBuffer str2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (str1) {
str1.append("a");
str2.append(1);
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (str2) {
str1.append("b");
str2.append(2);
System.out.println(str1);
System.out.println(str2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (str2) {
str1.append("c");
str2.append(3);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (str1) {
str1.append("d");
str2.append(4);
System.out.println(str1);
System.out.println(str2);
}
}
}
}).start();
}
}
线程a拿到str1的锁(同步监视器),因为有嵌套的同步代码块,所以要结束当前a线程,必须要拿到str2的锁。
而线程b与线程a同时进行,b线程拿到str2的锁,也是因为嵌套的同步代码块,所有要结束当前b线程,必须要拿到str1的锁
因为a线程拿到了str1的锁,所以b线程无法得到str1的锁,所以b线程阻塞,等待str1的锁释放
同理,b线程拿到str2的锁,所以a线程无法得到str2的锁,所以a线程阻塞,等待str2的锁释放
两个线程各自拿着对方的锁,所以造成线程死锁
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
线程安全问题 — JDK5.0新增
方式三:Lock锁
public class ThreadCreateLockTest {
public static void main(String[] args) {
Window7 window7 = new Window7();
Thread t1 = new Thread(window7);
Thread t2 = new Thread(window7);
Thread t3 = new Thread(window7);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window7 implements Runnable{
private int ticket = 100;
// 实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
// 实例化ReentrantLock,参数为true,让他们公平竞争,当线程a出来之后,不会立马重新进行争夺,等后面的进程完成之后再重新争夺
//private ReemtrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
// 调用锁定方法lock()
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
// 调用解锁方法unlock()
lock.unlock();
}
}
}
}
- 在线程中实例化ReentranLock对象,然后调用lock() 方法上锁,再调用unlock() 方法解锁。
- 操作的代码部分要用try-finally包含,finally中写unlock();
synchronized与Lock对比
线程面试题1
- synchronized与lock的异同
- 相同:二者都可以解决线程安全问题
- 不同
- synchronized机制在执行完相应的同步代码之后,自动释放同步监视器
- lock需要手动的启动同步监视器( lock() ),同时结束同步也需要手动的实现( unlock() )
线程的通信
线程之间的交互
使用wait()和notify()/notifyAll()
-
wait() 将线程进入阻塞状态,同时线程释放同步监视器
-
notify() 执行此方法,就会唤醒被wait() 的一个线程,如果有多个线程被wait() 那么就会根据优先级唤醒一个线程