一、内部类及匿名内部类
成员变量可以被匿名内部类访问
final修饰的局部变量可以被匿名内部类访问,其他修饰类型不可以
public interface Test01 {
void eat();
}
public class Test02 implements Test01{
@Override
public void eat() {
System.out.println("重写Test01后的eat方法");
}
}
public class Test03 {
public static void main(String[] args) {
//传统方式
Test01 t=new Test02();
t.eat();
/**
* 匿名内部类
* {};实现了Test01接口
*/
Test01 test01 = new Test01() {
@Override
public void eat() {
System.out.println("重写Test01后的匿名内部类的方法");
}
};
test01.eat();
}
}
注意:
直接new接口时必须重写接口的方法。虽然直观上看是直接new的Person。但是实际是创建了Person的内部类,而不能说接口可以实例化。
二、使用匿名内部类的场景
因为匿名内部类的特性,只能在实例化的地方使用。所以匿名内部类主要应用场景:
1. 只提供了接口或抽象类。必须自己编写子类,且之类就用一次。
2. 事件回调
具体情况使用场景:
1. Java 线程
2. AWT事件
3. Android开发中
三、Swing案例—龟兔赛跑
Swing案例是为了说明匿名内部类的事件回调写法
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
public class GTRun {
static boolean isWin=false;
public static void main(String[] args) {
//创建窗口,指定标题
JFrame jFrame = new JFrame("龟兔赛跑");
//设置窗口大小
jFrame.setSize(500,400);
//窗口,有默认的布局,居中-----清除默认的布局
jFrame.setLayout(null);
/**
* 创建兔子的图标,并添加到标签中,将图片的标签放到窗口中
*/
//创建兔子图标
ImageIcon tzimageIcon = new ImageIcon("D:\\ASXT\\javaSE02\\Thread\\tuzi.jpg");
//并添加到标签中
JLabel tzjlabel = new JLabel(tzimageIcon);
tzjlabel.setBounds(0,100,50,50);
//将图片的标签放到窗口中
jFrame.add(tzjlabel);
/**
* 创建乌龟的图标,并添加到标签中,将图片标签放到窗口中
*/
//创建乌龟图标
ImageIcon wgimageIcon=new ImageIcon("D:\\ASXT\\javaSE02\\Thread\\wugui.jpg");
//并添加到标签中
JLabel wgjLabel = new JLabel(wgimageIcon);
wgjLabel.setBounds(0,150,50,50);
//将图片的标签放到窗口中
jFrame.add(wgjLabel);
/**
* 龟兔同时开跑
* 龟:线程
* 1.乌龟每次移动0-5像素,移动后随机休眠0--500毫秒,到达窗口右侧停止
* 兔:线程
* 2.兔子每次移动0-10像素,移动后随机休眠0--1000毫秒,到达窗口右侧停止
*/
Thread tzThread=new Thread(()->{
try {
while (true){
//获取兔子当前的位置
int x=tzjlabel.getX();
int i=new Random().nextInt(11);
tzjlabel.setBounds(x+i,100,50,50);
//终点
if (x>=350){
/**
* isWin:false
* !isWin:true
* 如果兔子到达终点时,还没有人到达重点
* isWin-true
* 如果兔子到达终点时,有人到达终点
*/
if (!isWin){
isWin=true;
JOptionPane.showMessageDialog(null,"兔子胜利","龟兔赛跑结果",JOptionPane.ERROR_MESSAGE);
}
break;//跳出当前循环
}
Thread.sleep(new Random().nextInt(11)*100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread wgThread=new Thread(()->{
try {
while (true){
//获取乌龟当前的位置
int x=wgjLabel.getX();
int i=new Random().nextInt(6);
wgjLabel.setBounds(x+i,200,50,50);
//终点
if (x>=350){
if (!isWin){
isWin=true;
JOptionPane.showMessageDialog(null,"乌龟胜利","龟兔赛跑结果",JOptionPane.ERROR_MESSAGE);
}
break;//跳出当前循环
}
Thread.sleep(new Random().nextInt(6)*100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
/**
* 添加按钮
*/
JButton jButton=new JButton("开始");
jButton.setBounds(200,0,100,50);
jFrame.add(jButton);
/**
* 事件:
* 用户和程序之间的交互行为,如:鼠标单机 鼠标双击 按键盘
* 事件回调:自动去调用某一些方法
* 事件监听:监听触发了哪些事件
*/
jButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
//点击开始按钮开启兔子和乌龟的赛跑线程
tzThread.start();
wgThread.start();
}
});
//窗口关闭,程序也关闭
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//窗口展示
jFrame.setVisible(true);
}
}
四、线程介绍
1. 进程
进程:系统中正在运行的一个应用程序。当在系统中启动一个程序后,系统就会为该程序分配一个进程。
2. 线程
线程是操作系统中的概念。
线程是操作系统运行调度的最小单位。一个进程中至少会有一个线程。一个线程就是程序代码的顺序执行的过程,代码逐行执行,执行时下面代码必须等待上面代码执行完成。
在java中主方法对应的就是程序的主线程。当启动应用程序运行主方法就是运行在线程中。一个类中只能存在一个主方法,所以只能有一个主线程。
3. 线程的休眠
在一个线程中的代码都是顺序执行的,上面的代码执行完成才会执行下面的代码。这也叫做线程的阻塞性。
使用输出语句可以感受线程的顺序执行,但是不能感觉到阻塞性。为了能够更清除明白线程的阻塞性,可以通过Thread.sleep();方法感受
Thread中sleep方法中
1. 实现功能:哪个线程调用sleep()方法, 就让哪个线程休眠多少毫秒。给人的感觉是执行 Thread.sleep(n); 时花费了n毫秒, 实际为该线程休眠了n毫秒
(注:1000毫秒=1秒)
2. native关键字表示方法的实现调用了另一 种编程语言的代码。具体说明底层使用C语言实现的。
3. 因为方法是static的,所以可以直接使用类名调用。
4. 参数是休眠多少毫秒。
4. 多线程
操作系统内部支持多进程,而每个进程的内部又是支持多线程的,线程是轻量的,新建线程会共享所在进程的系统资源,因此目前主流的开发都是采用多线程。多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制。
多线程:一个进程中包含多个线程
多线程的调度, 执行等都是由硬件CPU负责的
利用多线程,让程序具备了多任务处理能力。
我们可以在主线程中创建多个线程,这些线程称为子线程。
子线程不会阻塞主线程的执行,子线程中代码可以和主线程中代码同时执行。
5. 并发与并行
并发:一个处理器同时处理多个任务,同一时间间隔处理(时间片轮转算法)
并行:多个处理器或者多核任务处理器同时处理多个任务,同时处理
6. 应用程序、进程、线程的关系
一个应用程序对应一个进程
一个进程可以包含多个线程
所以:一个程序关闭了,在系统中的进程就没了,程序中的线程也就结束了。
五、线程的创建
1. Thread类的概念
java.lang.Thread类代表线程,任何线程对象都是Thread类的实例
Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性。
2. 创建方式
在java中创建线程有三种实现方式。
1.继承Thread类,重写run方法:创建线程后,调用start()方法开启线程, 自动的调用 重写之后Thread的run方法
2.实现runnable接口,重写run方法:创建线程后,调用start(),首先调用Thread类中的run方法,然后调用Runnable接口实现类中的run方
3.实现Callable接口,结合Future
使用Futuretask类当作线程对象和Callable实现类的桥梁
Futuretask实现了Runnable接口,实现了run方法,
在run方法实现类中调用了Callable接口实现类中的call()方法,
* Futuretask中还封装了返回值,通过get()获取返回值
3.实现方式
3.1 继承Thread类
public class Thread01Imp extends Thread{
@Override
public void run() {
System.out.println("子线程"+Thread.currentThread().getName());
}
}
//继承Thread类,重写run方法
public class Thread01 {
public static void main(String[] args) {
//使用Thread无参构造方法创建子线程,开启一个子线程
Thread thread=new Thread01Imp();
//线程执行
thread.start();
/**
* 匿名内部类
*继承Thread,重写run方法
*/
Thread thread1 = new Thread("自定义") {
@Override
public void run() {
System.out.println("自定义的线程执行了"+Thread.currentThread().getName());
}
};
//线程执行
thread1.start();
//获取当前执行的线程
System.out.println(Thread.currentThread().getName());
System.out.println("主线程执行了");
}
}
3.2 实现Runnable接口
public class Thread02Imp implements Runnable{
@Override
public void run() {
System.out.println("子线程"+Thread.currentThread().getName());
}
}
public class Thread02 {
public static void main(String[] args) {
/**
* 自定义Runable实现类,实现Runable中的run方法
*/
Thread02Imp thread02Imp=new Thread02Imp();
Thread thread=new Thread(thread02Imp);
thread.start();
/**
* 使用匿名内部类,创建Runable接口的实现类,实现run方法
*/
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类实现runnable接口" + Thread.currentThread().getName());
}
};
Thread thread1=new Thread(runnable);
thread1.start();
//简便写法
new Thread(runnable).start();
//最简便的写法--lambda表达式
new Thread(()->{
System.out.println("实现runnable接口" + Thread.currentThread().getName());
}).start();
System.out.println(Thread.currentThread().getName()+"主线程执行了");
}
}
3.3 实现Callable接口
public class CallableImpl implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("调用了callable中的call 子线程");
return 7;
}
}
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 实现callable接口,实现call方法
* Futuretask类当作线程对象和callable实现类的桥梁
* futuretask实现了Runnable接口,实现了Run方法
* 在Run方法实现类中调用了callable接口实现类中的call()方法
* Futuretask中还封装了返回值,通过get()获取返回值
*/
public class Thread03 {
public static void main(String[] args) {
try {
CallableImpl callable=new CallableImpl();
//创建和Thread交互的桥梁
//FutureTask是Runable实现类
FutureTask<Integer> integerfutureTask=new FutureTask<>(callable);
/**
* 创建线程()需要传递Runable的实现类
* Futuretask是Runnable接口的实现类
*/
Thread thread=new Thread(integerfutureTask);
thread.start();
Integer integer = integerfutureTask.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
六、JMM的三大特征
1. 什么是JMM
内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象描述,不同架构下的物理机拥有不一样的内存模型,Java虚拟机(jvm)是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)
2. 线程/工作内存/主内存 三者的关系
- 所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问
2. 每个线程都有一个独立的工作内存,用于存储线程私有的数据
3. 线程对变量的操作(读取赋值等)必须在工作内存中进行。(线程安全问题的根本原因)
a. 首先要将变量从主内存拷贝到线程的工作内存中, 不允许直接操作主内存中的变量
b. 每个线程操作自己工作内存中的变量副本,操作完成后再将变量写回主内存
c. 多个线程对一个共享变量进行修改时,都是对自己工作内存中的副本进行操作,相互不可见, 所以主内存最后得到的结果是不可预知的
d. 因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成3. JMM的三个特征
3.1 可见性
在Java中,不同线程拥有各自的私有工作内存,当线程需要读取或修改某个变量时,不能直接去操作主内存中变量,而是需要将这个变量读取到该线程的工作内存中(变量副本),当该线程修改其变量副本后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值,
原因: 工作内存和主内存存在同步延迟
解决方案:
1. volatile: 可以保证内存可见性
① 规定线程每次修改变量副本后立刻同步到主内存中,用于保证其它线程可以看到自己对变量的修改
② 规定线程每次使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
2. synchronize: 同步锁
注意:
1. volilate可以保证可见性,但是假设多线程同时在做a++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是23.2 有序性
在单线程程序里代码逐行执行,但是在多线程并发时,程序的执行就有可能出现乱序,在多线程执行程序时, 为了提高性能,编译器和处理器常常会自动对指令做重排序,目的是进行相关的优化, 指令重排序使得代码在多线程执行时可能会出现一些问题
例如:
源代码:
int a = 1;
int b =2;
a = 3;
b = 4;
优化后:
int a = 1;
a = 3;
int b =2;
b = 4;
解决方案:
1. volatile: 在指令序列中插入内存屏障(一组处理器指令)来防止指令重排序,有序性
2. synchronize: 同步锁
3.3 原子性
原子性指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个线程的操作一旦开始,就不会被其他线程干扰, 只能当前线程执行完, 其他线程才可以去执行
解决方案: 1. synchronize: 同步锁3.4 总结
- volatile
变量在每次被线程访问时,都会立即从主内存中读该变量的值,而当该变量发生变化时,又会将最新的值立即刷新到主内存,不同的线程总是能够看到该变量的最新值, 除特殊情况(如: 有线程A和线程B, A和B都从主内存中将变量a=0, 加载到了自己的工作内存中, A将a=0变为a=1, 立即刷新主内存, 此时B正在操作a=0变为a=1, 立即刷新也没有任何的作用, 最后的正确结果为a=2, 但是结果却为a=1),
可以解决指令重排的问题, 但是不能解决原子性
2. synchronized同步锁
可以解决可见性, 有序性, 原子性