1:什么是线程
程序按顺序执行,程序执行的线索就是一条线程!
2:创建线程的方式
创建线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。
1:继承Thread类来实现线程
此处启动线程可以使用mythread1.start()方法,也可以用一个Thread对象来承载mythread1这个对象,然后调用Thread对象的start方法,因为Thread类是实现了Runnable接口的
public class ThreadTest {
static public void main(String... args){
MyThread1 myThread1 = new MyThread1();
Thread thread = new Thread(myThread1);
thread.start();
}
}
class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("我是一个线程");
}
}
- 运行结果:我是一个线程
2:实现Runnable接口来创建线程
如果是实现Runnable接口,那么就只能用Thread类来承载Mythread2对象并调用start方法来开启线程
public class ThreadTest {
static public void main(String... args){
MyThread2 myThread2 = new MyThread2();
Thread thread = new Thread(myThread2);
thread.start();
}
}
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("我是一个线程");
}
}
- 运行结果:我是一个线程
每个线程只能调用start方法一次,如果调用两次,那么编译时不会报错,运行时会报异常
3:两种创建线程方法的比较
这两种方式都能创建线程,并且都能让线程启动起来,那么为什么平时在使用的时候会用到第二种方式(实现Runnable接口)多一些呢?
- JAVA只支持单继承,如果设计类时还需要继承其他类,那么用继承Thread的方式就不能达到同时继承两个类
- JAVA可以实现多个接口,那么可以在继承了其他类的情况下通过实现接口的方式来达到创建线程
- 使用Runnale方式更加的面向对象,就好比我线程这个对象要start需要一个Runnable对象,你给我一个Runnable对象就行
4:原子性
原子性就是说一个操作不可以被中途cpu暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行; 如果一个操作是原子性的,那么在多线程境下,就不会出现变量被修改等奇怪的问题
如何保证一个动作的原子性呢?接下来所说的同步代码块就能保证操作原子性
5:同步代码块(synchronized)
当需要对某块逻辑进行原子性操作时,可以用这个关键字将代码框起来,这个关键字也叫对象锁,只有或得了锁得对象才能够执行该逻辑,其他得线程来到此逻辑后,就在等待池里排队,此排队时无序得,并且释放锁的对象再次抢到该锁的几率比较大
以一个卖票为例,要求三个窗口同时售卖100张票,实现代码如下:
public class ThreadTest {
static public void main(String... args){
Ticket ticket = new Ticket();
for (int i = 1; i <= 3; i++ ){
new Thread(ticket).start();
}
}
}
class Ticket implements Runnable{
// 定义静态变量,总票数
private static Integer nums = 100;
@Override
public void run() {
while (true){
synchronized (this){
if (nums > 0){
System.out.println(Thread.currentThread().getName()+"已经卖出去第"+nums+"张票。");
// 卖出去一张票,nums数量-1
nums--;
// 此处睡眠一秒是为了让效果更加明显
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
- 运行结果:
Thread-0已经卖出去第100张票。
Thread-2已经卖出去第99张票。
Thread-1已经卖出去第98张票。
Thread-1已经卖出去第97张票。
Thread-2已经卖出去第96张票。
Thread-2已经卖出去第95张票。
Thread-2已经卖出去第94张票。
Thread-0已经卖出去第93张票。
Thread-0已经卖出去第92张票。
如果去掉synchronized,那么运行会出现两个线程同时卖一张票的情况,这不符合我们的要求。例如去掉会输出这样:
Thread-1已经卖出去第100张票。
Thread-2已经卖出去第100张票。
Thread-0已经卖出去第99张票。
Thread-0已经卖出去第97张票。
显然同一张票是不能卖出去两次的,接下来我们聊聊synchronized后面的参数。这个里面可以放任意对象来当锁;为什么?首先来了解下synchronized的原理。
注意在JVM中,有个计数器的,如果调用了某个带synchronized的方法,计数器+1,一开始是0,如果在执行该方法时候又调用了其他synchronized方法,那么计数器再+1,方法执行结束返回,计数器-1,直到计数器为0,表示这个对象的锁被释放了。
由此可见,只要保证当锁的对象是同一个对象就行,而不用担心是什么对象。如:
class Cat{
}
class Ticket implements Runnable{
// 定义静态变量,总票数
private static Integer nums = 100;
Cat cat = new Cat();
@Override
public void run() {
while (true){
synchronized (cat) {
if (nums > 0) {
System.out.println(Thread.currentThread().getName() + "已经卖出去第" + nums + "张票。");
// 卖出去一张票,nums数量-1
nums--;
// 此处睡眠一秒是为了让效果更加明显
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
自定义一只猫来当锁的对象也可以实现同步效果。
6:死锁
当一个线程获得锁之后,计数器加1,然后同步代码块里面的逻辑会去调用另一个线程,然后另一个线程的锁被其他线程拿走了,所以就会在线程等待池等待,但是另一个线程却再自己取得锁的线程等待池里面,然后就会造成死锁。
设计代码逻辑时,应当有清晰的逻辑思路,避免死锁的产生