java面试题之2

线程深入
  1. 线程和进程的区别:
    1. 进程是什么?进程是程序执行的一个实例。每一个进程都有自己的独立的一块内存空间、一组资源系统。其内部数据和状态都是完全独立的。系统的引导的时候会开启很多服务,这些服务就叫做守护进程,也叫后台服务程序,它的生命周期较长,在系统关闭时终止。
    2. 线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
  2. 线程的状态:
    1. 创建状态:就是Thread t=new Thread(Runnable tagret);表示创建一个线程
    2. 就绪状态:t1.start()表示线程准备就绪,随时抢占cpu资源,注意一点,调用start()方法并不是立刻就开始执行,这个是要看cpu调度的。
    3. 运行状态:Thread里面的run()方法正在运行二等状态,也就是线程的业务开始执行
    4. 阻塞状态:在多线程中,每个线程得到cpu使用权基本一样,每个线程都要抢占cpu,当一个线程没有被cpu调度的时候,它就会处于阻塞状态,等待cpu的调度。
    5. 终止状态:线程执行完毕或者调用了stop()方法使得现场终止。
  3. Thread线程实现的两种方法:
    1. 继承Thread类并覆写run()方法,实例化对象后就可以调用。
    2. 实现Runnable接口,覆写run()方法,实例化对象的形式是Thread t1=new Thread(Runnable taget)target是实现了Runnable接口的子类实例。
  4. 使用Thread继承和runnable实现的线程的区别:使用Thread类的时候,无法实现资源的共享,使用Runnable的时候能实现资源共享。也就是说,多个线程共同操作一个资源。但是,这样不能保证线程之间的同步,也就是说,可能这个线程在操作这个资源的时候,另外一个线程也在操作这个资源。怎么说,一个线程在执行的时候,这个线程本身可能有很多的操作步骤,但是由于线程的执行机制,这个线程在执行的时候,可能只执行了几步就被叫停,其他线程接手执行。这样就会造成资源的具体情况在各个线程之间有延迟。所以需要进行代码块的同步,使得这个代码块是一次全部完成的,也就是一个线程得到cup调度的时候,它可以执行完全部代码,之后再交出使用权。在买票的系统里面,只有一个线程可以将所有的票卖完。这是我们自己的代码的问题。
  5. 一个对象有一个锁,当一个线程访问一个对象的同步方法或者同步代码块的时候,它就会获得这个对象的锁,其他线程如果在同时间也想访问的话,就得等待那个线程执行完同步代码块后释放该对象锁。如果是不同对象的话,那么他们之间其实不存在锁的竞争关系的。则他们的输出其实和没有同步的输去规则是一样的,但是这时候的数据是安全的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class MyDemon2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Info info=new Info();//同一个对象
Info info1=new Info();//同一个对象
new MyThread(info).start();
new MyThread(info).start();
new MyThread(info1).start();
new MyThread(info1).start();
}
}
class Info{
private int money=5;
private int ticket=6;
public synchronized void show1(){
System.out.println(Thread.currentThread().getName()+"获取对象锁");
for(int x=0;x<10;x++){
if(money>0){
System.out.println("money的值为"+money--);
}
}
money=5;
}
public synchronized void show2(){
System.out.println(Thread.currentThread().getName()+"获取对象锁");
for(int x=0;x<10;x++){
if(ticket>0){
System.out.println("money的值为"+ticket--);
}
}
}
}
class MyThread extends Thread{
private Info info=null;
public MyThread(Info info){
this.info=info;
}
@Override
public void run() {
// TODO Auto-generated method stub
info.show1();
}
}

由输出结果可以知道,线程0和线程1是竞争关系,线程2和3是竞争关系,所有0和1,2和3是一部打印内容的,但是0和2,3还有1和2,3是没有关系的,他们之间就是没有同步的时候的输出情况了,但是此时他们是没有竞争关系的,更谈不上对数据的破坏。

  1. 类锁。当同步静态方法的时候,我们使用的是类锁,一个类的静态变量和方法是所有对象共享的,也就是和对象无关。所以即使是不同的对象访问同步的静态方法,他们之间的关系也是竞争关系。
  2. 进一步了解线程:多个对象多个线程之间的关系。多个线程访问同一个对象的同步代码块的时候,锁是由竞争关系的,所以是那个线程先获得对象锁先访问,当竞争的线程越多的时候,随机性越明显。总之就是同一个对象的时候,是竞争关系,这时候的输出是看那个线程先得到对象的锁的。在测试的代码里面,如果我们的线程比较少的时候,它的访问顺序似乎和代码的顺序是有关的,其实没有关系的。你将竞争的线程多开几个就可以知道是随机的。
  3. 使用Lock接口实现代码块的同步,在这里我们知道,存在竞争对象的线程,其实只在同步代码块那一部分有锁的竞争,其他地方是不存在的。也就是synchronized(){}前后的代码是不同步的。即使是在run方法里面。
  4. 通过消费者和生产者模式进行深度了解:class Info {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
private String nameString;
private String content;
private boolean flag=false;//线程工作标志位,每个线程都在改变这个标志位
//生产消息:
public synchronized void setMsg(String name,String cont){
if(flag){
try {
System.out.println("生产的线程"+Thread.currentThread().getName()+"等待,有产品存在!");
super.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("开始生产");
this.nameString=name;
this.content=cont;
flag=true;
super.notify();
}
public synchronized void getMsg(){
if(!flag){
try {
System.out.println("消费的线程"+Thread.currentThread().getName()+"等待,没有产品存在!");
super.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("消费:"+nameString+"-->"+content);
flag=false;
super.notify();
}
}
class Product implements Runnable{
private Info info;
boolean flag=false;
public Product(Info info){
this.info=info;
}
@Override
public void run() {//生产者一次生成两个产品。
// TODO Auto-generated method stub
for(int i=0;i<20;i++){
if(flag){
info.setMsg("产品1", "食品");
flag=false;
}else{
info.setMsg("产品2", "日用品");
flag=true;
}
}
}
}
class Customer implements Runnable{
private Info info=null;
public Customer(Info info) {
// TODO Auto-generated constructor stub
this.info=info;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<20;i++){
info.getMsg();
}
}
}
public class TestDemon {
public static void main(String[] args) {
Info info=new Info();//同一个对象
Product pro=new Product(info);
Customer cus=new Customer(info);
new Thread(pro).start();
new Thread(cus).start();
}
}

从上面的例子我们可以知道,其实这个就是对线程的认为调度,也就是使得线程一次执行,轮流执行。那么是怎么办到的呢?其实很简单,那就是使用wait()和notify()对线程进行唤醒和等待(线程阻塞),之后就是线程的唤醒。如果只有两个线程,那么就是唤醒就行,而不是唤醒所有。一个线程执行完后就唤醒其他线程,这样每次只有一个线程在执行。
之一wiat()方法后面的语句是在线程被唤醒后执行的。在同步的代码块里面,线程如果被wiat()的话,那么这个线程是暂停在此处,当被唤醒的时候,会继续执行下去。
消费者和生产者的产品对象是同一个对象,两个线程之间是存在竞争关系的。但是,一个线程获得一个对象锁的时候(其实就是得到执行synchronized代码块的去权利),它在执行的时候,我们让他进行判断,使得它存在 执行 或者 wait()阻塞状态。每次执行完一个同步代码块后,两个线程都会再次进行竞争。

理解run()在线程里的执行。
当我们对一个线程启动start()的时候,其实就是运行我们的run()方法体里面的代码,里面的代码在线程的执行的时候,是竞争关系的,虽然大家最后都完成任务,但是过程确是随机的。我们的run()里面如果出现sychronized代码块,那么只是这个代码块里面的内容是同步的,其他的代码还是继续抢占资源的存在。同步代码块的执行就是给获得执行机会的线程执行完这个代码块的权利,此时其他线程是无法操作这些代码的。同时也无法操作该对象里面的其他sychronized代码,也就是说只有一个线程可以操作所有的sychronized代码,但是对于没有同步的代码,线程是可以同时操作的。
理解我们的生产者和消费者模式的设计:线程同时进入两个run里面,执行代码,但是由于是对象同步的,如果没有进行线程的控制的话(wait()和notify()操作)只是单纯的执行sychronized的话,由于同一个线程可以多次获得一个对象的锁,所有会存在消费者消费完该产品的时候,会再次消费该产品,因为此时消费的线程再次获得锁访问资源,但是该资源还没有更新。(一般是消费者消费后,该线程要进入wait()模式等待数据更新,如果该线程再次获得锁进入来,那么由于标志位的反转,它就得处于wait()阻塞状态)。