java 线程学习笔记(一)
一、简单的例子
1.实现Runnable
public class RunnableTest implements Runnable {
private String name;
public RunnableTest(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 3; i++) {
try {
// for(int t = 0;t<1000000000;t++);
Thread.sleep(1000);
System.out.println(name + ": " + i);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[]args){
RunnableTest runnableTest1 = new RunnableTest("test1");
RunnableTest runnableTest2 = new RunnableTest("test2");
RunnableTest runnableTest3 = new RunnableTest("test3");
RunnableTest runnableTest4 = new RunnableTest("test4");
new Thread(runnableTest1).start();
new Thread(runnableTest2).start();
new Thread(runnableTest3).start();
new Thread(runnableTest4).start();
}
}
运行结果
test3: 0
test1: 0
test4: 0
test2: 0
test3: 1
test1: 1
test4: 1
test2: 1
test1: 2
test3: 2
test4: 2
test2: 2
2.继承Thread
public class ThreadTest extends Thread {
public ThreadTest(String name){
super(name);
}
public void run(){
for (int i = 0; i < 5; i++) {
try {
for(int t = 0;t<1000000000;t++);
System.out.println(this.getName() + ": " + i);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[]args){
Thread threadTest1 = new ThreadTest("test1");
Thread threadTest2 = new ThreadTest("test2");
Thread threadTest3 = new ThreadTest("test3");
Thread threadTest4 = new ThreadTest("test4");
threadTest1.start();
threadTest2.start();
threadTest3.start();
threadTest4.start();
}
}
运行结果
test1: 0
test2: 0
test3: 0
test4: 0
test1: 1
test2: 1
test3: 1
test4: 1
test1: 2
test3: 2
test2: 2
test4: 2
test1: 3
test3: 3
test2: 3
test1: 4
test4: 3
test3: 4
test2: 4
test4: 4
3.二者有什么区别呢。。。
在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:
避免点继承的局限,一个类可以继承多个接口。
适合于资源的共享
3.1以卖票程序为例,通过Thread类完成
public class TicketsThreadTest extends Thread {
private int ticket=5;
public void run(){
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println("卖出一张 剩余票数 :"+ (--this.ticket));
}
}
}
public static void main(String[] args){
Thread t1 = new TicketsThreadTest();
Thread t2 = new TicketsThreadTest();
Thread t3 = new TicketsThreadTest();
t1.start();
t2.start();
t3.start();
}
}
运行结果
卖出一张 剩余票数 :4
卖出一张 剩余票数 :4
卖出一张 剩余票数 :4
卖出一张 剩余票数 :3
卖出一张 剩余票数 :3
卖出一张 剩余票数 :3
卖出一张 剩余票数 :2
卖出一张 剩余票数 :2
卖出一张 剩余票数 :1
卖出一张 剩余票数 :2
卖出一张 剩余票数 :0
卖出一张 剩余票数 :1
卖出一张 剩余票数 :0
卖出一张 剩余票数 :1
卖出一张 剩余票数 :0
相当于三个窗口,互不干扰,各卖各的。。
3.2以卖票程序为例,通过Runnable类完成
public class TicketsRunnableTest implements Runnable {
private int ticket=5;
public void run(){
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println("卖出一张 剩余票数 :"+ (--this.ticket));
}
}
}
public static void main(String[] args){
TicketsRunnableTest r1 = new TicketsRunnableTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
Thread t3 = new Thread(r1);
t1.start();
t2.start();
t3.start();
}
}
运行结果
卖出一张 剩余票数 :4
卖出一张 剩余票数 :2
卖出一张 剩余票数 :0
卖出一张 剩余票数 :3
卖出一张 剩余票数 :1
实现资源共享。
二者的联系:
点进Thread类->
public
class Thread implements Runnable
...
二、常见问题
1、线程的名字,一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是mian,非主线程的名字不确定。
2、线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。
3、获取当前线程的对象的方法是:Thread.currentThread();
4、在上面的代码中,只能保证:每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。
--系统是怎么判断线程执行顺序
--Java 中 JVM 的线程调度方式不一定是抢占式的(它要依赖操作系统的线程调度),要具体看 JVM 所在的操作系统,也有可能是协作试的。(Windows 确实是抢占式的)而给线程分配时间片是实现抢占式线程调度的一种手段。优先级越高的线程获得的运行机会越多。
--start 方法只是将线程切换到可运行状态,不代表启动后线程就马上开始运行,有可能当 main 线程执行结束后(此时4个线程都已启动)它才被线程调度程序撤下 CPU,此时因为n个线程优先级相同,所以它们竞争着去争夺运行机会,至于谁抢到要看线程调度器的调度。所以不会按顺序依次执行的。
可是
书上说的的情况大多是在单核处理器上,但不完全对,那个线程会执行,完全取决于操作系统,
操作系统有自己的处理机制,Java会跟操作系统商量,优先级高的线程比优先级低的线程先执行的概率相对高一些,但不是绝对的,有时候优先级低的会先执行,完全取决于操作系统;
对于双核处理器,优先级高的线程比优先级低的线程先执行的概率逐渐减小,优先级高的线程和优先级低的线程都有可以先执行;
对于多核处理器,优先级高的线程和优先级低的线程哪个会先执行,真心不好说;另外多核处理器设置线程优先级没太多意义。
测试电脑为双核:
public static void main(String[] args){
TicketsRunnableTest r1 = new TicketsRunnableTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
Thread t3 = new Thread(r1);
t1.setPriority(3);
t2.setPriority(2);
t3.setPriority(1);
t1.start();
t2.start();
t3.start();
}
运行结果
Thread-0卖出一张 剩余票数 :4
Thread-0卖出一张 剩余票数 :1
Thread-0卖出一张 剩余票数 :0
Thread-2卖出一张 剩余票数 :2
Thread-1卖出一张 剩余票数 :3
这样的运行结果该如何解释
卖出一张 剩余票数 :4
卖出一张 剩余票数 :2
卖出一张 剩余票数 :1
卖出一张 剩余票数 :0
卖出一张 剩余票数 :3
卖出一张 剩余票数 :-1
5、当线程目标run()方法结束时该线程完成。
--线程结束后,理解为不再在队列中排队了
6、一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。
--再次启动会抛异常的
7、线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。
众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。
8、尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列唱呢个一个队列的事实。
9、尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
三、Java线程:线程栈模型与线程的变量
要理解线程调度的原理,以及线程执行过程,必须理解线程栈模型。
线程栈是指某时刻时内存中线程调度的栈信息,当前调用的方法总是位于栈顶。线程栈的内容是随着程序的运行动态变化的,因此研究线程栈必须选择一个运行的时刻(实际上指代码运行到什么地方)。
下面通过一个示例性的代码说明线程(调用)栈的变化过程。
这幅图描述在代码执行到两个不同时刻1、2时候,虚拟机线程调用栈示意图。
当程序执行到t.start();时候,程序多出一个分支(增加了一个调用栈2),这样,栈1、栈2并行执行。
从这里就可以看出方法调用和线程启动的区别了。
五、Java线程:线程的同步与锁
1. 同步问题提出
线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏
public class Person {
private int age = 100;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int subAge(int num){
age = age - num;
return age;
}
}
public class RunnableAgeTest implements Runnable {
private Person person =new Person();
public static void main(String[] args) {
RunnableAgeTest r = new RunnableAgeTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
public void run() {
for (int i = 0; i < 5; i++) {
person.subAge(20);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " :当前人的年龄 " + person.getAge());
}
}
}
运行结果
Thread-0 :当前人的年龄 60
Thread-1 :当前人的年龄 60
Thread-1 :当前人的年龄 20
Thread-0 :当前人的年龄 20
Thread-0 :当前人的年龄 -20
Thread-1 :当前人的年龄 -40
Thread-0 :当前人的年龄 -60
Thread-1 :当前人的年龄 -80
Thread-1 :当前人的年龄 -100
Thread-0 :当前人的年龄 -100
两个线程同时访问Person对象并修改数据
2.同步和锁定
1、锁的原理
Java中每个对象都有一个内置锁
当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。
一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
释放锁是指持锁线程退出了synchronized同步方法或代码块。
关于锁和同步,有以下几个要点:
1)、只能同步方法,而不能同步变量和类;
2)、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程*访问而不受锁的限制。
6)、线程睡眠时,它所持的任何锁都不会释放。
7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。例如:
public int subAge(int num){
synchronized (this){
age = age - num;
}
return age;
}
当然,同步方法也可以改写为非同步方法,但功能完全一样的,例如:
public synchronized int getAge() {
return age;
}
与
public int getAge() {
synchronized(this){
return age;
}
}
效果是完全一样的。
运行结果
Thread-1 :当前人的年龄 60
Thread-0 :当前人的年龄 60
Thread-1 :当前人的年龄 20
Thread-0 :当前人的年龄 20
Thread-0 :当前人的年龄 -20
Thread-1 :当前人的年龄 -20
Thread-0 :当前人的年龄 -60
Thread-1 :当前人的年龄 -60
Thread-0 :当前人的年龄 -100
Thread-1 :当前人的年龄 -100
。。。感觉不太对
...
转载于:https://my.oschina.net/u/2449014/blog/511685
上一篇: 使用URLClassLoader调用外部jar,并执行指定类方法
下一篇: 线程的基本操作