0.前言
上一篇文章,我们讲解了ArrayList的相关用法
看本文之前,推荐先去看一遍该文章今天我们主要讲解线程
若想要了解“类”等其他主要知识,可以去看前面的文章
- 由于最终目的是要开发安卓app,
因此这里使用的IDE是AS(Android Studio)
(不会使用的可以参考下面这篇文章中的例子)
《[Java]开发安卓,你得掌握的Java知识2》
1.文章主要内容
线程的基础概念
多线程的使用方法
多线程的同步
线程安全
2.基础知识讲解
2.1线程与进程
一个程序的运行就是一个进程
比如,QQ运行了,QQ就是一个进程一个进程中有许多线程,
比如使用QQ的时候,接受信息,打开聊天框,下载文件等,都是不同的线程一个进程中必定会有一个主线程
线程之间是共享(由进程申请的)内存资源的
2.2线程的一些细节
为什么要创建子线程:
- 如果在主线程中存在有比较耗时的操作:
下载视频 、上传文件等操作
为了不阻塞主线程,需要将耗时的任务放在子线程中去处理- 一个线程有可能处于不同的状态:
状态名字 | 具体描述 |
---|---|
NEW | 新建状态,即线程刚被创建好 |
RUNNABLE | 就绪状态,即只要抢到时间片,就可以运行这个线f程 |
BLOKCED | 阻塞状态,即通过sleep()或wait()暂停线程 |
WAITING | 等待状态 |
TIMED_WAITING | 这个以后再说 |
TERMINATED | 终止状态 |
如何创建子线程
方法1.继承Thread的类
- 创建一个继承于Thread的类
class TestThread extends Thread{
}
- 创建好后,要重写Thread中的run方法
class TestThread extends Thread{
@Override
public void run() {
//获得当前线程名字
String name = Thread.currentThread().getName();
//一个循环输出
for (int i = 0; i < 10; i++) {
System.out.println(name + " " + (i + 1));
}
}
}
- 在主函数中创建TestThread类的对象,并且调用start()方法
(不是run(),原因是start()才是开启子线程的方法,但是开启线程后run()肯定会被调用)- 线程的run()方法其实运行在当前线程,而不会单独开启一个子线程
public static void main(String[] args){
Thread t = new TestThread();
t.start();
}
输出结果为:
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
Thread-0 10
- Thread-0就是这个线程的名字
如何创建子线程
方法2.实现Runnable接口
思路:创建一个类实现Runnable接口,然后将这个类的对象作为参数放在Thread的构造函数中,调用Thread对象的构造方法
- 先创建一个类,实现Runnable接口
class PXDThread implements Runnable{
}
- 在这个类中实现run方法(与第一种方法一样)
class PXDThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
(1)在主函数中,先建立PXDThread的一个对象
(2)使用Thread来操作这个任务
(3)启动这个线程
//1.创建一个任务:创建一个类实现Runnable接口
PXDThread pt = new PXDThread();
//2.使用Thread来操作这个任务
Thread t = new Thread(pt);
//3.启动这个线程
//setName可以给线程起名字
t.setName("子线程1");
t.start();
输出结果为:
子线程1 0
子线程1 1
子线程1 2
子线程1 3
子线程1 4
子线程1 5
子线程1 6
子线程1 7
子线程1 8
子线程1 9
方法2.2
- 如果这个方法只要执行一次,可以考虑用匿名类的方式
而不用单独地额外创建一个类
public static void main(String[] args){
Thread t3 =new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " "+i);
}
}
});
t3.setName("子线程3");
t3.start();
}
输出结果为:
子线程3 0
子线程3 1
子线程3 2
子线程3 3
子线程3 4
子线程3 5
子线程3 6
子线程3 7
子线程3 8
子线程3 9
方法2.3匿名类的时候就使用start()
- 如果这个方法只要执行一次,且想要更简便的表达式
可以考虑在匿名类的后面加.start()
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " "+i);
}
}
}).start();
}
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
Thread-0 10
注意:
- 使用匿名类是没有办法为线程设置名字的
方法2.4使用Lambda表达式
- 如果这个方法只要执行一次,且想要更简便的表达式
可以考虑使用Lambda表达式
public static void main(String[] args){
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " "+i);
}
}).start();
}
- 优点:十分简洁
-
缺点:对于初级开发人员而言过于复杂难懂
2.5线程的同步
- 试想一下,如果一个卖票网站还剩一张票,两个人同时买票,
由于线程是同时执行的,那会不会发生一张票卖给两个人呢?- 会的话,应该怎么解决呢
class Ticket implements Runnable{
//一共一百张票
public static int NUM = 100;
public String name;
public Ticket(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (NUM > 0) {
//当多个线程操作的时候,可能线程1会先打印,然后时间片被抢去,线程2会打印然后减减,
//时间片还给线程1之后,NUM--,这就会导致少了一张票,而且两个线程还出现了重票
//多线程并没有办法知道什么时候被打断
System.out.println(name + "出票: " + (NUM + 1));
NUM--;
}else{
break;
}
}
}
public static void main(String[] args) {
Ticket ticket1 = new Ticket("重庆");
Thread t1 = new Thread(ticket1);
t1.start();
Ticket ticket2 = new Ticket("上海");
Thread t2 = new Thread(ticket2);
t2.start();
}
输出结果为:
重庆出票: 100
上海出票: 100
重庆出票: 99
上海出票: 98
重庆出票: 97
上海出票: 96
.......
(为了观感这里就不全部列出结果,没必要)
- 我们可以看到,两个线程同时运行的时候,很可能会卖出重复的票(如重庆和上海都卖出了编号为100的这张票),原因是线程的时间片的获得,与失去是随机的
线程的同步
就是为了解决多个线程同时运行的时候,某个线程不知道在何时会被抢走时间片无法继续执行,导致一些错误
(比如卖重票,少卖了票等)
保证线程同步的方法:
方法1.synchornized
方法1.1使用同步代码块
static final Object obj = new Object();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//圆括号中放一个监听器/对象
synchronized (obj){
if (NUM > 0) {
System.out.println(name + "出票: " + (NUM));
NUM--;
}
}
}
输出结果为:
重庆出票: 100
重庆出票: 99
重庆出票: 98
重庆出票: 97
重庆出票: 96
重庆出票: 95
......
重庆出票: 9
重庆出票: 8
重庆出票: 7
上海出票: 6
上海出票: 5
上海出票: 4
上海出票: 3
上海出票: 2
上海出票: 1
(为了观感不全部列出)
加了锁之后,可以看到不会卖出重复票了
注意:
每一个对象都有一个自己的锁,因此 synchronized后面的括号可以放任意一个对象
但是为了前后两个线程使用的是同一个锁,则
定义的obj必须为static静态类型,以保证它先于类被创建,且所有对象用的是同一个obj变量
方法1.2使用同步方法
- 使用同步方法的实质就是在使用同步代码块,只是写法不同
public void run() {
test();
}
public synchronized void test(){
for (int i = 0; i < 100; i++) {
if (NUM > 0) {
System.out.println(name + "出票: " + (NUM + 1));
NUM--;
} else {
break;
}
}
}
这段代码相当于(当然这么写编译器过不了):
synchronized (this) {
test();
}
public void test(){
for (int i = 0; i < 100; i++) {
if (NUM > 0) {
System.out.println(name + "出票: " + (NUM + 1));
NUM--;
} else {
break;
}
}
}
- 这里这么写的话必须保证是同一个this,就是说得保证是同一个对象调用run()
但是上述例子其实不能保证,所以这个例子是没有办法用这种写法的
3.实例应用
- 要求:
(1)用代码模拟客户找房屋中介买房子,中介找到合适的房子后返回消息给客户
(2)希望这件事情在一个子线程中实现
代码思路:
(1)首先写一个Person类,一个Agent类,Agent类要继承Thread,这样才能在子线程中执行
(2)Person类的对象要有一个方法A,来调用Agent类的方法B
(3)Agent类的这个方法B完成寻找房屋的过程,然后返回消息给Person对象
(4)这个“返回消息”靠的是接口回调,即Agent类中定义一个接口,以及定义一个接口变量,Person类实现这个接口。
(5)方法A中要把this赋值给Agent类的接口变量,然后在方法B的最后,就可以靠接口变量来调用Person类中实现的接口的方法了
,以此来(返回消息)告诉Person找到房子了
public class Agent extends Thread{
AgentInterface target;
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println("开始找房");
System.out.println("--------");
System.out.println("--------");
System.out.println("房子找到了,即将返回数据");
target.callBack("西南大学");//回调
super.run();
}
public interface AgentInterface{
void callBack(String desc);
}
}
- 继承Thread是因为“找房子的过程”要在子线程中实现
- target为接口变量,有了它才能实现接口回调
public class Person implements Agent.AgentInterface {
public void needHouse(){
Agent xw = new Agent();
xw.target = this;
xw.start();
}
//接口就是一种统一,让创建接口的类可以轻松地通过调用实现该接口的类的方法
//来告诉实现接口的类,已经做完事情了
//就好比中介会让客户都装一个微信,然后通过微信来告诉他们,这个微信,就是一个接口
@Override
public void callBack(String desc) {
System.out.println("我是小王,接收到你的数据了:" + desc);
}
}
needHouse()方法是为了能够调用Agent类的run()方法
如果Person类对象(设为person)想要在run()的最后能够告诉person能做完了的话,必须在Person中把this赋给Agent中的target
( xw.target = this;这句话是能够实现接口回调的关键)只有xw.target = this;,Agent中的run 方法中的
target.callBack("西南大学");(接口回调)才能够成立(接口回调说白了就是把消息返回给实现接口的类)
public static void main(String[] args) {
Person ls = new Person();
ls.needHouse();
}
输出结果为:
Thread-0
开始找房
房子找到了,即将返回数据
我是小王,接收到你的数据了:西南大学
- 其中,Thread-0就是该线程的名字,不是main就证明不是在主线程中执行的,而是单独开启了一个子线程
4.总结
(1)本文讲解了线程的概念、基本使用、线程安全等问题
(2)线程是Java中十分重要的一个知识点,只要看一些例子,再结合现实中的实际情况,线程的相关内容其实挺好理解的,语法也不难,可能只是第一次见到时,会感觉有点绕,关键就是要在实际情况中多去使用即可。