1.什么是线程?什么是进程?
进程:进程是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一次完整过程。
线程:每一个进程都有至少有一个执行顺序,该顺序是一个执行路径,也叫做一个控制单元。而一个独立的控制单元就是一个线程。
区别:一个进程有好多线程,比如word文档是一个进程,那拼写检查,就是一个线程,线程关闭了但进程不一定消失,但是进程关闭了,线程一定消失。
2.创建线程的两种方式
在java中如果想要实现多线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。
2.1继承Thread类
在java中,如果一个类继承了Thread类,此类就称为多线程实现类。在Thread的子类中,必须覆盖run()方法才行,因为线程要执行的内容都是要定义在run()方法内的。
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"运行: i="+i);
}
}
}
如果要启动线程,需要调用start()方法
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start();
thread2.start();
}
上面的代码定义了两个线程,并启动了它们,运行结果如下(其中的一种情况):
Thread-0运行: i=0
Thread-1运行: i=0
Thread-0运行: i=1
Thread-1运行: i=1
Thread-0运行: i=2
Thread-1运行: i=2
Thread-0运行: i=3
Thread-0运行: i=4
Thread-1运行: i=3
Thread-1运行: i=4
Thread-1运行: i=0
Thread-0运行: i=1
Thread-1运行: i=1
Thread-0运行: i=2
Thread-1运行: i=2
Thread-0运行: i=3
Thread-0运行: i=4
Thread-1运行: i=3
Thread-1运行: i=4
注意:启动线程必须调用start()方法,不能调用run()方法。而且start()方法只能被调用一次否则会报错。
2.2实现Runnable接口
创建线程的另一种方式是通过实现Runnable接口来实现多线程,Runnable接口中只定义了一个方法public void run(); 示例如下:
public class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"运行: i="+i);
}
}
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
Thread t1 = new Thread(thread1);
Thread t2 = new Thread(thread2);
t1.start();
t2.start();
}
}
2.3两种方式的联系与区别
既然java有两种方式创建多线程,那两者有什么联系和区别呢,如果查看Thread类源码会得到这样的总结(精简后的代码):
public class Thread {
public Thread(Runnable target, String name){
init(null, target, name, 0);
}
private void init(ThreadGroup,Runnable target, String name,long stackSize){
this.target = target;
}
public void run(){
if(target!=null){
target.run();
}
}
}
可以发现,在Thread类中的run()方法调用的是Runnable接口中的run()方法,也就是说此方法是由Runnable子类完成的,所以如果要是通过继承Thread类实现多线程则必须覆盖run()方法。
这是二者之间的联系,那么区别呢?Thread类不能资源共享。例如:
public class MyThread implements Runnable{
private int ticket = 5;
@Override
public void run() {
while(true){
if(ticket>0){
System.out.println("卖票: ticket=" + ticket--);
}
if(ticket==0){
break;
}
}
}
public static void main(String[] args) {
MyThread sell = new MyThread();
new Thread(sell).start();
new Thread(sell).start();
new Thread(sell).start();
}
}
运行结果为:
卖票: ticket=5
卖票: ticket=4
卖票: ticket=3
卖票: ticket=2
卖票: ticket=1
上面启动了3个线程,但是卖票却共同卖了5张票,换句话说就是ticket属性被所有线程共享了,而继承Thread类却做不到这一点。
卖票: ticket=4
卖票: ticket=3
卖票: ticket=2
卖票: ticket=1
上面启动了3个线程,但是卖票却共同卖了5张票,换句话说就是ticket属性被所有线程共享了,而继承Thread类却做不到这一点。
所以实现Runnable接口比继承Thread的好处有如下几点:
1. 适合多个相同的线程去处理同一资源
2. 可以避免java单继承特点带来的局限性
3. 增强了程序的健壮性,代码能被多个线程共享,代码与数据是独立的
3.线程的生命周期
线程的生命周期有五个状态
1. 创建状态
在程序中创建了一个线程对象后,这个线程对象便处于创建状态,此时它已经有了相应的内存空间和其他资源,但是出于不可运行状态。
2.就绪状态
当该线程调用start()方法后就启动了线程,此时线程具有了执行资格,但是CPU还没有调用这个线程,这时叫就绪状态
3.运行状态
当就绪状态的线程被调用的时候就同时具有了执行权和执行资格,线程就进入了运行状态,此时线程会执行run方法内的内容
4.阻塞状态
一个正在执行的线程在某些特殊的情况下被人为的挂起,此时CPU会暂停对该线程的执行,该线程进入阻塞状态。一般sleep(),wait()等方法都会使线程进入阻塞状态。
5.死亡状态
当线程调用stop()方法或者run()方法执行结束后,即出于死亡状态。
4.线程的同步和死锁
先观察如下代码:
private int ticket = 5;
@Override
public void run() {
while(true){
if(ticket>0){
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("卖票: ticket=" + ticket--);
}
if(ticket==0){
break;
}
}
}
运行结果为:
...
卖票: ticket=2
卖票: ticket=1
卖票: ticket=0
卖票: ticket=-1
卖票: ticket=2
卖票: ticket=1
卖票: ticket=0
卖票: ticket=-1
程序加入了延迟操作后,运行结果出现了负数。原因分析,当票数还剩1张的时候,线程1拿到了执行权发现票数大于0,则继续执行,执行到sleep()方法后暂时sleep 100毫秒,此时线程2获得执行权,线程2发现票数还是大于0,则也进入执行,执行到sleep(100)的时候也暂时进入睡眠,这线程1醒了过来拿到了执行权,继续执行剩下的部分,对票进行-1操作,此时票数已经为0了,线程1结束。线程2醒过来之后也继续执行,而此时ticket已经变为0,所以当线程2再对票数进行-1操作的时候就得到了-1。
如果想解决这个问题就必须用到线程的同步。
4.1线程的同步
解决资源共享的同步操作,可以使用同步代码块和同步方法两种方式来完成。
同步代码块的格式为:
private int ticket = 5;
@Override
public void run() {
synchronized (this) {
while(true){
if(ticket>0){
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("卖票: ticket=" + ticket--);
}
if(ticket==0){
break;
}
}
}
很多人多同步语句中的this不了解,synchronized (object)括号内放的是一个对象,有人会问,这个对象是哪个对象,任意一个对象都行吗?答案是任意一个对象都行,但是,这个对象就像是一把锁,一开始处于打开状态,如果一个线程进来了,这把锁就锁上了,其他线程发现锁被锁上了,就无法进入,当其中的线程运行完同步代码块的内容时,锁会自动打开,其他线程再进入再重复上面的步骤,所以 如果想实现同步必须保证多个线程用的是同一把锁。如果线程用的是不同的锁,同步就失效了。
在看同步方法的格式
public class MyThread implements Runnable{
private int ticket = 5;
@Override
public void run() {
this.sell();
}
public synchronized void sell(){
while(true){
if(ticket>0){
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("卖票: ticket=" + ticket--);
}
if(ticket==0){
break;
}
}
}
}
synchronized方法也是持有一把锁的,它默认持有的对象就是该类的对象,static方法也可以被synchronized修饰,如果被synchronized修饰则static方法持有的对象是这个类的字节码文件。
4.2死锁
所谓死锁就是指两个线程都在等对方先执行完,造成了停滞,一般死锁都是在运行的时候发生的,下面是个死锁的例子:
public class Ticket implements Runnable{
public static final Object obj = new Object();
public static final Object obj1 = new Object();
boolean flag;
public Ticket(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (obj) {
System.out.println("if-obj");
synchronized (obj1) {
System.out.println("if-obj1");
}
}
}
else{
synchronized (obj1) {
System.out.println("else-lockobj1");
synchronized (obj) {
System.out.println("else-lockobj");
}
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
Ticket t1 = new Ticket(true);
Ticket t2 = new Ticket(false);
Thread th1 = new Thread(t1);
Thread th2 = new Thread(t2);
th1.start();
th2.start();
}
}
线程1运行的时候会进入if语句,线程2则进入else,当线程1进入if语句第一个同步块的时候,持有obj锁,此时线程切换到线程2运行,线程2进入else语句中第一个同步块,线程2持有obj1锁。然后线程切换到线程1,线程1发现obj1锁正在被线程2使用则会在这个地方等待,当线程2执行的时候发现obj锁被线程1使用则也会等待,这样就造成了线程1和线程2都在等待对方执行完的情况,这就是死锁。