本文转自点击打开链接
本文主要讲解java多线程的基本概念,尽量用代码加以理解。最后给出阿里2015年4月份软件研发实习生笔试有关多线程的一道题的做法。
多线程是实现并发机制的有效手段,与进程相比,线程是划分得比进程更小的执行单元。此外,每个进程都有专用的内存区域,即进程之间数据和状态是完全独立的,而同一进程的线程之间,线程共享内存单元(包括代码和数据)。
Java多线程实现方式有2种,一种是通过继承Thread类实现;一种是通过实现Runnable接口实现多线程。下面直接贴代码:
需要首先指出的是:Thread类是Runnable接口的一个子类;而Runnable接口中只有一个run()方法,所以若按第一种继承实现,需要覆写run()方法,若按第二种实现接口,需要实现run()方法。
在启动线程时必须调用Thread类的start()方法,事实上,调用start()方法时,Java虚拟机开始执行run()方法。
所以按第二种方法实现时,才会使用一个Runnable接口的实例化对象作为参数去实例化一个Thread类对象。
public class ThreadDemo_1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
TestThread t = new TestThread();
t.start();
for(int i=0;i<10000000;i++)//大一点才可看出效果,不然一个时间片就可以执行完
{
System.out.println("MainThread-----is running");
}
}
}
//通过继承Thread类实现多线程
class TestThread extends Thread
{
public void run()
{
for(int i=0;i<10000000;i++)
{
System.out.println("TestThread-----is running");
}
}
}
public class ThreadDemo_11 {
public static void main(String[] args) {
// TODO Auto-generated method stub
TestThread t2 = new TestThread();
new Thread(t2).start();
for(int i=0;i<10000000;i++)//大一点才可看出效果,不然一个时间片就可以执行完
{
System.out.println("MainThread正在运行");
}
}
}
//通过实现Runnable接口实现多线程
class TestThread_2 implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<1000000;i++){
System.out.println("TestThread_2正在运行");
}
}
}
关于
这两种多线程机制实现的比较
,下面先给出比较说明,再举例说明。
尽量采用实现Runnable接口的方法实现多线程。1.可以避免Java单继承的局限性,如果继承了Thread类就不能继承其他类了,而实现Runnable接口则没有影响,事实上,接口正是实现Java多继承的机制。2.适合操纵同一个资源,实现资源共享的目的。3增强程序的健壮性,通过实现Runnable接口的类的实例实例化Thread类,使代码能够被多个线程共享。
下面以多个火车票销售点卖票为例说明这两种方式在操纵同一资源时的区别。
使用继承Thread类实现多线程:
public class ThreadDemo_2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
//虽然启动了3个线程,但是4个线程对象各自占有各自的资源 ,但可共享static资源
new TestThread_a().start();
new TestThread_a().start();
new TestThread_a().start();
}
}
class TestThread_a extends Thread
{
private int tickets = 10;
//private static int tickets = 10;//继承Thread类共享static变量
public void run()
{
while(true){
if(tickets>0)
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
}
}
}
部分结果如下:
可见,实际只有10张票,但各个站点(3个线程)一共卖了30张
使用Runnable接口实现多线程:
public class ThreadDemo_3 {
public static void main(String[] args) {
//启动了3个线程,同时实现了资源共享的目的。
//线程被构造时,都是通过一个接口实例实例化对象作为参数去实例化Thread对象,所以同一个实例下的线程可以资源共享
TestThread_b t = new TestThread_b();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class TestThread_b implements Runnable
{
private int tickets = 10;
public void run()
{
while(true){
if(tickets>0){
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
}
}
}
}
可见,三个站点一共卖了10张票。
比较可得:使用Thread类实际上并没有达到资源共享的目的;而使用Runnable接口实现多线程时,使用的是同一个实现Runnable接口的类实例作为构造方法的实参来构造线程,该实例下的实例变量等是共享的,所以能达到资源共享的目的;而使用Thread类new了3次构造3个线程,3个线程对象各自占有各自的资源。
在上述程序的结果中,事实上还出现了一张票被卖了多次的问题,即 线程同步 问题。主要可能的原因是在线程切换的过程中,一个线程的执行操作还没完成,如只判断没执行票减1操作,又转到另一个线程执行。
为了刻意造成这种现象,调用sleep方法使线程执行到该处后暂停进而执行别的进程;
public class ThreadDemo_3 {
public static void main(String[] args) {
//启动了3个线程,同时实现了资源共享的目的。
//线程被构造时,都是通过一个接口实例实例化对象作为参数去实例化Thread对象,所以同一个实例下的线程可以资源共享
TestThread_b t = new TestThread_b();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class TestThread_b implements Runnable
{
private int tickets = 10;
public void run()
{
while(true){
if(tickets>0){
//根本原因是资源数据访问不同步引起的
try{
Thread.sleep(100);//加了后引发多线程同步问题,一张票被卖了多次
}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
}
}
}
}
可见一张票被多个站点卖出。
为了解决这个问题,可以使用同步代码块,保证操作的原子性,同一时刻只能有一个线程进入同步代码块内执行;或同步方法。具体这里略过。
下面主要讲解 线程间通信
一个典型问题是生产者消费者问题;一个线程(生产者)向存储空间添加数据,另一个线程(消费者)从存储空间读取数据;
如一个线程不断向存储空间写入(小明,男)(小芳,女)信息;另一个线程不断从存储空间读数据。有3种写法如下所示:
case1:
package com;
//存数和取树不一致 ---资源不同步,还未写完便读取
class Producer implements Runnable
{
P q = null;
public Producer(P q)
{
this.q = q;
}
@Override
public void run() {
// TODO Auto-generated method stub
int i=0;
while(true){
if(i==0){
q.name = "小明";
q.sex = "男";
}
else{
q.name = "小芳";
q.sex = "女";
}
i = (i+1)%2;
}
}
}
class Consumer implements Runnable{
P q = null;
public Consumer(P q){
this.q = q;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
System.out.println(q.name+"---->"+q.sex);
}
}
}
class P
{
String name = "小芳";
String sex = "女";
}
public class ThreadCommunation {
public static void main(String[] args) {
// TODO Auto-generated method stub
P q = new P();
new Thread(new Producer(q)).start();//放苹果
new Thread(new Consumer(q)).start();//取苹果
}
}
case 2:
package Thread;
//存数和取数一致 ---但是重复读取
class Producer implements Runnable
{
P q = null;
public Producer(P q)
{
this.q = q;
}
@Override
public void run() {
// TODO Auto-generated method stub
int i=0;
while(true){
if(i==0){
q.set("小明", "男");
}
else{
q.set("小芳", "女");
}
i = (i+1)%2;
}
}
}
class Consumer implements Runnable{
P q = null;
public Consumer(P q){
this.q = q;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
q.get();
}
}
}
class P
{
private String name = "小芳";
private String sex = "女";
public synchronized void set(String name,String sex){
this.name = name;
this.sex = sex;
}
public synchronized void get(){
System.out.println(this.name+"--->"+this.sex);
}
}
public class ThreadCommunation1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
P q = new P();
new Thread(new Producer(q)).start();
new Thread(new Consumer(q)).start();
}
}
从结果中可以看出,case1 写法出现了小明是女 小芳是男的奇怪现象;原因是小明姓名写入,但是性别还未写入,就转入另外一个线程的执行,这就是资源不同步的原因。
case 2 引入get()和set同步方法使读和写变为同步操作,不会出现读写错误的问题,但是总是重复读写同一数据,这在实际中不必要的。下面先介绍几个线程间通信的几个方法,都是Object类中的,可以直接调用。
wait():导致线程进入等待状态直到它被通知,只能在同步方法中使用。
notify():随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态,只能在同步方法或同步块中使用。
notifyAll():解除那些在该对象上调用wait()方法的线程,解除其阻塞状态,只能在同步方法或同步块中使用。
设置一个标志位Full;只有其为真时表示存储空间为空可以放入(不能读),否则表示已有数据不能放入(可以读)。
package Thread2;
//存数和取树一致 ---但是重复读取
class Producer1 implements Runnable
{
P1 q = null;
public Producer1(P1 q)
{
this.q = q;
}
@Override
public void run() {
// TODO Auto-generated method stub
int i=0;
while(true){
if(i==0){
q.set("小明", "男");
}
else{
q.set("小芳", "女");
}
i = (i+1)%2;
}
}
}
class Consumer1 implements Runnable{
P1 q = null;
public Consumer1(P1 q){
this.q = q;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
q.get();
}
}
}
class P1
{
private String name = "小芳";
private String sex = "女";
boolean Full = false;
public synchronized void set(String name,String sex){
if(Full){
try{
wait();
}catch(InterruptedException e){}
}
this.name = name;
try{
Thread.sleep(10);
}catch(Exception e){
System.out.println(e.getMessage());
}
this.sex = sex;
Full = true;
notify();
}
public synchronized void get(){
if(!Full){
try{
wait();
}catch(InterruptedException e){}
}
System.out.println(this.name+"--->"+this.sex);
Full = false;
notify();
}
}
public class ThreadCommunation2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
P1 q = new P1();
new Thread(new Producer1(q)).start();
new Thread(new Consumer1(q)).start();
}
}
最后给出阿里巴巴今年暑假软件研发实习生招聘的一道笔试题。
题目为:用Java模拟实现,一个人不断往箱子放苹果,另一个人不断从箱子中取苹果;箱子最多能放5个苹果,不能使用java.util.concurrent包中的类。
下面是我的一种写法,如有不对之处,请指正。
package Thread2;
class Producer implements Runnable
{
P q = null;
public Producer(P q)
{
this.q=q;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
q.put();
}
}
}
class Consumer implements Runnable{
P q = new P();
public Consumer(P q){
this.q = q;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
q.get();
}
}
}
class P
{
private static int count=0;
public synchronized void put(){
if(count==5){
try{
wait();
}catch(InterruptedException e){}
}
try{
Thread.sleep(100);
}catch(Exception e){
System.out.println(e.getMessage());
}
count++;
System.out.println("放入一个苹果,剩:"+count);
notify();
}
public synchronized void get(){
if(count==0){
try{
wait();
}catch(InterruptedException e){}
}
count--;
System.out.println("取出一个苹果,剩:"+count);
notify();
}
}
public class Apple {
public static void main(String[] args) {
// TODO Auto-generated method stub
P q = new P();
new Thread(new Producer(q)).start();
new Thread(new Consumer(q)).start();
}
}